mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-04 16:52:38 +00:00
Merge branch 'master' into fixFacingFlowPrintIssues
This commit is contained in:
@@ -3,14 +3,15 @@ import React, { useEffect, useState } from 'react';
|
||||
const BrewUtils = require('./brewUtils/brewUtils.jsx');
|
||||
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
|
||||
import AuthorUtils from './authorUtils/authorUtils.jsx';
|
||||
import LockTools from './lockTools/lockTools.jsx';
|
||||
|
||||
const tabGroups = ['brew', 'notifications', 'authors'];
|
||||
const tabGroups = ['brew', 'notifications', 'authors', 'locks'];
|
||||
|
||||
const Admin = ()=>{
|
||||
const [currentTab, setCurrentTab] = useState('brew');
|
||||
const [currentTab, setCurrentTab] = useState('');
|
||||
|
||||
useEffect(()=>{
|
||||
setCurrentTab(localStorage.getItem('hbAdminTab'));
|
||||
setCurrentTab(localStorage.getItem('hbAdminTab') || 'brew');
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
@@ -40,6 +41,7 @@ const Admin = ()=>{
|
||||
{currentTab === 'brew' && <BrewUtils />}
|
||||
{currentTab === 'notifications' && <NotificationUtils />}
|
||||
{currentTab === 'authors' && <AuthorUtils />}
|
||||
{currentTab === 'locks' && <LockTools />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
342
client/admin/lockTools/lockTools.jsx
Normal file
342
client/admin/lockTools/lockTools.jsx
Normal file
@@ -0,0 +1,342 @@
|
||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./lockTools.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
|
||||
import request from '../../homebrew/utils/request-middleware.js';
|
||||
|
||||
const LockTools = createClass({
|
||||
displayName : 'LockTools',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
fetching : false,
|
||||
reviewCount : 0
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
this.updateReviewCount();
|
||||
},
|
||||
|
||||
updateReviewCount : async function() {
|
||||
const newCount = await request.get('/api/lock/count')
|
||||
.then((res)=>{return res.body?.count || 'Unknown';});
|
||||
if(newCount != this.state.reviewCount){
|
||||
this.setState({
|
||||
reviewCount : newCount
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateLockData : function(lock){
|
||||
this.setState({
|
||||
lock : lock
|
||||
});
|
||||
},
|
||||
|
||||
render : function() {
|
||||
return <div className='lockTools'>
|
||||
<h2>Lock Count</h2>
|
||||
<p>Number of brews currently locked: {this.state.reviewCount}</p>
|
||||
<button onClick={this.updateReviewCount}>REFRESH</button>
|
||||
<hr />
|
||||
<LockTable title='Locked Brews' text='Total Locked Brews' resultName='lockedDocuments' fetchURL='/api/locks' propertyNames={['shareId', 'title']} loadBrew={this.updateLockData} ></LockTable>
|
||||
<hr />
|
||||
<LockTable title='Brews Awaiting Review' text='Total Reviews Waiting' resultName='reviewDocuments' fetchURL='/api/lock/reviews' propertyNames={['shareId', 'title']} loadBrew={this.updateLockData} ></LockTable>
|
||||
<hr />
|
||||
<LockBrew key={this.state.lock?.key || 0} lock={this.state.lock}></LockBrew>
|
||||
<hr />
|
||||
<div style={{ columns: 2 }}>
|
||||
<LockLookup title='Unlock Brew' fetchURL='/api/unlock' updateFn={this.updateReviewCount}></LockLookup>
|
||||
<LockLookup title='Clear Review Request' fetchURL='/api/lock/review/remove'></LockLookup>
|
||||
</div>
|
||||
<hr />
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
const LockBrew = createClass({
|
||||
displayName : 'LockBrew',
|
||||
getInitialState : function() {
|
||||
// Default values
|
||||
return {
|
||||
brewId : this.props.lock?.shareId || '',
|
||||
code : this.props.lock?.code || 455,
|
||||
editMessage : this.props.lock?.editMessage || '',
|
||||
shareMessage : this.props.lock?.shareMessage || 'This Brew has been locked.',
|
||||
result : {},
|
||||
overwrite : false,
|
||||
};
|
||||
},
|
||||
|
||||
handleChange : function(e, varName) {
|
||||
const output = {};
|
||||
output[varName] = e.target.value;
|
||||
this.setState(output);
|
||||
},
|
||||
|
||||
submit : function(e){
|
||||
e.preventDefault();
|
||||
if(!this.state.editMessage) return;
|
||||
const newLock = {
|
||||
overwrite : this.state.overwrite,
|
||||
code : parseInt(this.state.code) || 100,
|
||||
editMessage : this.state.editMessage,
|
||||
shareMessage : this.state.shareMessage,
|
||||
applied : new Date
|
||||
};
|
||||
|
||||
request.post(`/api/lock/${this.state.brewId}`)
|
||||
.send(newLock)
|
||||
.set('Content-Type', 'application/json')
|
||||
.then((response)=>{
|
||||
this.setState({ result: response.body });
|
||||
})
|
||||
.catch((err)=>{
|
||||
this.setState({ result: err.response.body });
|
||||
});
|
||||
},
|
||||
|
||||
renderInput : function (name) {
|
||||
return <input type='text' name={name} value={this.state[name]} onChange={(e)=>this.handleChange(e, name)} autoComplete='off' required/>;
|
||||
},
|
||||
|
||||
renderResult : function(){
|
||||
return <>
|
||||
<h3>Result:</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.keys(this.state.result).map((key, idx)=>{
|
||||
return <tr key={`${idx}-row`}>
|
||||
<td key={`${idx}-key`}>{key}</td>
|
||||
<td key={`${idx}-value`}>{this.state.result[key].toString()}
|
||||
</td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function() {
|
||||
return <div className='lockBrew'>
|
||||
<div className='lockForm'>
|
||||
<h2>Lock Brew</h2>
|
||||
<form onSubmit={this.submit}>
|
||||
<label>
|
||||
ID:
|
||||
{this.renderInput('brewId')}
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
Error Code:
|
||||
{this.renderInput('code')}
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
Private Message:
|
||||
{this.renderInput('editMessage')}
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
Public Message:
|
||||
{this.renderInput('shareMessage')}
|
||||
</label>
|
||||
<br />
|
||||
<label className='checkbox'>
|
||||
Overwrite
|
||||
<input name='overwrite' className='checkbox' type='checkbox' value={this.state.overwrite} onClick={()=>{return this.setState((prevState)=>{return { overwrite: !prevState.overwrite };});}} />
|
||||
</label>
|
||||
<label>
|
||||
<input type='submit' />
|
||||
</label>
|
||||
</form>
|
||||
{this.state.result && this.renderResult()}
|
||||
</div>
|
||||
<div className='lockSuggestions'>
|
||||
<h2>Suggestions</h2>
|
||||
<div className='lockCodes'>
|
||||
<h3>Codes</h3>
|
||||
<ul>
|
||||
<li>455 - Generic Lock</li>
|
||||
<li>456 - Copyright issues</li>
|
||||
<li>457 - Confidential Information Leakage</li>
|
||||
<li>458 - Sensitive Personal Information</li>
|
||||
<li>459 - Defamation or Libel</li>
|
||||
<li>460 - Hate Speech or Discrimination</li>
|
||||
<li>461 - Illegal Activities</li>
|
||||
<li>462 - Malware or Phishing</li>
|
||||
<li>463 - Plagiarism</li>
|
||||
<li>465 - Misrepresentation</li>
|
||||
<li>466 - Inappropriate Content</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='lockMessages'>
|
||||
<h3>Messages</h3>
|
||||
<ul>
|
||||
<li><b>Private Message:</b> This is the private message that is ONLY displayed to the authors of the locked brew. This message MUST specify exactly what actions must be taken in order to have the brew unlocked.</li>
|
||||
<li><b>Public Message:</b> This is the public message that is displayed to the EVERYONE that attempts to view the locked brew.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
const LockTable = createClass({
|
||||
displayName : 'LockTable',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
title : '',
|
||||
text : '',
|
||||
fetchURL : '/api/locks',
|
||||
resultName : '',
|
||||
propertyNames : ['shareId'],
|
||||
loadBrew : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
result : '',
|
||||
error : '',
|
||||
searching : false
|
||||
};
|
||||
},
|
||||
|
||||
lockKey : React.createRef(0),
|
||||
|
||||
clickFn : function (){
|
||||
this.setState({ searching: true, error: null });
|
||||
|
||||
request.get(this.props.fetchURL)
|
||||
.then((res)=>this.setState({ result: res.body }))
|
||||
.catch((err)=>this.setState({ result: err.response.body }))
|
||||
.finally(()=>{
|
||||
this.setState({ searching: false });
|
||||
});
|
||||
},
|
||||
|
||||
updateBrewLockData : function (lockData){
|
||||
this.lockKey.current++;
|
||||
const brewData = {
|
||||
key : this.lockKey.current,
|
||||
shareId : lockData.shareId,
|
||||
code : lockData.lock.code,
|
||||
editMessage : lockData.lock.editMessage,
|
||||
shareMessage : lockData.lock.shareMessage
|
||||
};
|
||||
this.props.loadBrew(brewData);
|
||||
},
|
||||
|
||||
render : function () {
|
||||
return <>
|
||||
<div className='brewsAwaitingReview'>
|
||||
<div className='brewBlock'>
|
||||
<h2>{this.props.title}</h2>
|
||||
<button onClick={this.clickFn}>
|
||||
REFRESH
|
||||
<i className={`fas ${!this.state.searching ? 'fa-search' : 'fa-spin fa-spinner'}`} />
|
||||
</button>
|
||||
</div>
|
||||
{this.state.result[this.props.resultName] &&
|
||||
<>
|
||||
<p>{this.props.text}: {this.state.result[this.props.resultName].length}</p>
|
||||
<table className='lockTable'>
|
||||
<thead>
|
||||
<tr>
|
||||
{this.props.propertyNames.map((name, idx)=>{
|
||||
return <th key={idx}>{name}</th>;
|
||||
})}
|
||||
<th>clip</th>
|
||||
<th>load</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.result[this.props.resultName].map((result, resultIdx)=>{
|
||||
return <tr className='row' key={`${resultIdx}-row`}>
|
||||
{this.props.propertyNames.map((name, nameIdx)=>{
|
||||
return <td key={`${resultIdx}-${nameIdx}`}>
|
||||
{result[name].toString()}
|
||||
</td>;
|
||||
})}
|
||||
<td className='icon' title='Copy ID to Clipboard' onClick={()=>{navigator.clipboard.writeText(result.shareId.toString());}}><i className='fa-regular fa-clipboard'></i></td>
|
||||
<td className='icon' title='View Lock details' onClick={()=>{this.updateBrewLockData(result);}}><i className='fa-regular fa-circle-down'></i></td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
});
|
||||
|
||||
const LockLookup = createClass({
|
||||
displayName : 'LockLookup',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
fetchURL : '/api/lookup'
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
query : '',
|
||||
result : '',
|
||||
error : '',
|
||||
searching : false
|
||||
};
|
||||
},
|
||||
|
||||
handleChange(e){
|
||||
this.setState({ query: e.target.value });
|
||||
},
|
||||
|
||||
clickFn(){
|
||||
this.setState({ searching: true, error: null });
|
||||
|
||||
request.put(`${this.props.fetchURL}/${this.state.query}`)
|
||||
.then((res)=>this.setState({ result: res.body }))
|
||||
.catch((err)=>this.setState({ result: err.response.body }))
|
||||
.finally(()=>{
|
||||
this.setState({ searching: false });
|
||||
});
|
||||
},
|
||||
|
||||
renderResult : function(){
|
||||
return <div className='lockLookup'>
|
||||
<h3>Result:</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.keys(this.state.result).map((key, idx)=>{
|
||||
return <tr key={`${idx}-row`}>
|
||||
<td key={`${idx}-key`}>{key}</td>
|
||||
<td key={`${idx}-value`}>{this.state.result[key].toString()}
|
||||
</td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function() {
|
||||
return <div className='brewLookup'>
|
||||
<h2>{this.props.title}</h2>
|
||||
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='share id' />
|
||||
<button onClick={this.clickFn}>
|
||||
<i className={`fas ${!this.state.searching ? 'fa-search' : 'fa-spin fa-spinner'}`} />
|
||||
</button>
|
||||
|
||||
{this.state.error
|
||||
&& <div className='error'>{this.state.error.toString()}</div>
|
||||
}
|
||||
|
||||
{this.state.result && this.renderResult()}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LockTools;
|
||||
66
client/admin/lockTools/lockTools.less
Normal file
66
client/admin/lockTools/lockTools.less
Normal file
@@ -0,0 +1,66 @@
|
||||
.lockTools {
|
||||
.lockBrew {
|
||||
columns : 2;
|
||||
|
||||
.lockForm {
|
||||
break-inside : avoid;
|
||||
|
||||
label {
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
line-height : 2.25em;
|
||||
text-align : right;
|
||||
input {
|
||||
float : right;
|
||||
width : 65%;
|
||||
margin-left : 10px;
|
||||
}
|
||||
&.checkbox {
|
||||
line-height: 1.5em;
|
||||
input {
|
||||
width : 1.5em;
|
||||
height : 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lockSuggestions {
|
||||
line-height : 1.2em;
|
||||
break-inside : avoid;
|
||||
columns : 2;
|
||||
h2 { column-span : all; }
|
||||
h3 { margin-top : 0px; }
|
||||
b { font-weight : 600; }
|
||||
|
||||
.lockCodes { break-inside : avoid; }
|
||||
}
|
||||
}
|
||||
|
||||
.lockTable {
|
||||
cursor : default;
|
||||
break-inside : avoid;
|
||||
.row:hover {
|
||||
color : #000000;
|
||||
background-color : #CCCCCC;
|
||||
}
|
||||
.icon {
|
||||
cursor : pointer;
|
||||
&:hover { text-shadow : 0px 0px 6px black; }
|
||||
}
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding : 4px 10px;
|
||||
text-align : center;
|
||||
}
|
||||
table, td { border : 1px solid #333333; }
|
||||
|
||||
.brewLookup {
|
||||
min-height : 175px;
|
||||
break-inside : avoid;
|
||||
h2 { margin-top : 0px; }
|
||||
}
|
||||
|
||||
button i { padding-left : 5px; }
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
||||
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
/*=======--- Example CSS styling ---=======*/
|
||||
@@ -22,6 +23,13 @@ const DEFAULT_STYLE_TEXT = dedent`
|
||||
color: black;
|
||||
}`;
|
||||
|
||||
const DEFAULT_SNIPPET_TEXT = dedent`
|
||||
\snippet example snippet
|
||||
|
||||
The text between \`\snippet title\` lines will become a snippet of name \`title\` as this example provides.
|
||||
|
||||
This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme.
|
||||
`;
|
||||
let isJumping = false;
|
||||
|
||||
const Editor = createClass({
|
||||
@@ -36,6 +44,7 @@ const Editor = createClass({
|
||||
onTextChange : ()=>{},
|
||||
onStyleChange : ()=>{},
|
||||
onMetaChange : ()=>{},
|
||||
onSnipChange : ()=>{},
|
||||
reportError : ()=>{},
|
||||
|
||||
onCursorPageChange : ()=>{},
|
||||
@@ -52,7 +61,7 @@ const Editor = createClass({
|
||||
getInitialState : function() {
|
||||
return {
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text' //'text', 'style', 'meta'
|
||||
view : 'text' //'text', 'style', 'meta', 'snippet'
|
||||
};
|
||||
},
|
||||
|
||||
@@ -62,12 +71,11 @@ const Editor = createClass({
|
||||
isText : function() {return this.state.view == 'text';},
|
||||
isStyle : function() {return this.state.view == 'style';},
|
||||
isMeta : function() {return this.state.view == 'meta';},
|
||||
isSnip : function() {return this.state.view == 'snippet';},
|
||||
|
||||
componentDidMount : function() {
|
||||
|
||||
this.updateEditorSize();
|
||||
this.highlightCustomMarkdown();
|
||||
window.addEventListener('resize', this.updateEditorSize);
|
||||
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
@@ -82,10 +90,6 @@ const Editor = createClass({
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.updateEditorSize);
|
||||
},
|
||||
|
||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||
|
||||
this.highlightCustomMarkdown();
|
||||
@@ -118,14 +122,6 @@ const Editor = createClass({
|
||||
}
|
||||
},
|
||||
|
||||
updateEditorSize : function() {
|
||||
if(this.codeEditor.current) {
|
||||
let paneHeight = this.editor.current.parentNode.clientHeight;
|
||||
paneHeight -= SNIPPETBAR_HEIGHT;
|
||||
this.codeEditor.current.codeMirror.setSize(null, paneHeight);
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentCursorPage : function(cursor) {
|
||||
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
|
||||
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||
@@ -146,17 +142,17 @@ const Editor = createClass({
|
||||
|
||||
handleViewChange : function(newView){
|
||||
this.props.setMoveArrows(newView === 'text');
|
||||
|
||||
this.setState({
|
||||
view : newView
|
||||
}, ()=>{
|
||||
this.codeEditor.current?.codeMirror.focus();
|
||||
this.updateEditorSize();
|
||||
}); //TODO: not sure if updateeditorsize needed
|
||||
});
|
||||
},
|
||||
|
||||
highlightCustomMarkdown : function(){
|
||||
if(!this.codeEditor.current) return;
|
||||
if(this.state.view === 'text') {
|
||||
if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
|
||||
const codeMirror = this.codeEditor.current.codeMirror;
|
||||
|
||||
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||
@@ -175,12 +171,18 @@ const Editor = createClass({
|
||||
|
||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||
|
||||
let userSnippetCount = 1; // start snippet count from snippet 1
|
||||
let editorPageCount = 1; // start page count from page 1
|
||||
|
||||
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
||||
const whichSource = this.state.view === 'text' ? this.props.brew.text : this.props.brew.snippets;
|
||||
_.forEach(whichSource?.split('\n'), (line, lineNumber)=>{
|
||||
|
||||
const tabHighlight = this.state.view === 'text' ? 'pageLine' : 'snippetLine';
|
||||
const textOrSnip = this.state.view === 'text';
|
||||
|
||||
//reset custom line styles
|
||||
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
||||
codeMirror.removeLineClass(lineNumber, 'background', 'snippetLine');
|
||||
codeMirror.removeLineClass(lineNumber, 'text');
|
||||
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
||||
|
||||
@@ -191,22 +193,24 @@ const Editor = createClass({
|
||||
|
||||
// Styling for \page breaks
|
||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||
(this.props.renderer == 'V3' && line.match(PAGEBREAK_REGEX_V3))) {
|
||||
(this.props.renderer == 'V3' && line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3))) {
|
||||
|
||||
if(lineNumber > 0) // Since \page is optional on first line of document,
|
||||
if((lineNumber > 0) && (textOrSnip)) // Since \page is optional on first line of document,
|
||||
editorPageCount += 1; // don't use it to increment page count; stay at 1
|
||||
else if(this.state.view !== 'text') userSnippetCount += 1;
|
||||
|
||||
// add back the original class 'background' but also add the new class '.pageline'
|
||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||
codeMirror.addLineClass(lineNumber, 'background', tabHighlight);
|
||||
const pageCountElement = Object.assign(document.createElement('span'), {
|
||||
className : 'editor-page-count',
|
||||
textContent : editorPageCount
|
||||
textContent : textOrSnip ? editorPageCount : userSnippetCount
|
||||
});
|
||||
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
||||
};
|
||||
|
||||
|
||||
// New Codemirror styling for V3 renderer
|
||||
if(this.props.renderer == 'V3') {
|
||||
if(this.props.renderer === 'V3') {
|
||||
if(line.match(/^\\column$/)){
|
||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||
}
|
||||
@@ -462,6 +466,21 @@ const Editor = createClass({
|
||||
userThemes={this.props.userThemes}/>
|
||||
</>;
|
||||
}
|
||||
|
||||
if(this.isSnip()){
|
||||
if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; }
|
||||
return <>
|
||||
<CodeEditor key='codeEditor'
|
||||
ref={this.codeEditor}
|
||||
language='gfm'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.snippets}
|
||||
onChange={this.props.onSnipChange}
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
},
|
||||
|
||||
redo : function(){
|
||||
@@ -502,7 +521,7 @@ const Editor = createClass({
|
||||
historySize={this.historySize()}
|
||||
currentEditorTheme={this.state.editorTheme}
|
||||
updateEditorTheme={this.updateEditorTheme}
|
||||
snippetBundle={this.props.snippetBundle}
|
||||
themeBundle={this.props.themeBundle}
|
||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
||||
updateBrew={this.props.updateBrew}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
.editor {
|
||||
position : relative;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
container : editor / inline-size;
|
||||
|
||||
.codeEditor {
|
||||
height : 100%;
|
||||
.pageLine {
|
||||
.CodeMirror { height : 100%; }
|
||||
.pageLine, .snippetLine {
|
||||
background : #33333328;
|
||||
border-top : #333399 solid 1px;
|
||||
}
|
||||
@@ -14,6 +15,10 @@
|
||||
float : right;
|
||||
color : grey;
|
||||
}
|
||||
.editor-snippet-count {
|
||||
float : right;
|
||||
color : grey;
|
||||
}
|
||||
.columnSplit {
|
||||
font-style : italic;
|
||||
color : grey;
|
||||
@@ -104,3 +109,7 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@container editor (width < 553px) {
|
||||
.editor .codeEditor .CodeMirror { height : calc(100% - 51px);}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
import { loadHistory } from '../../utils/versionHistory.js';
|
||||
import { brewSnippetsToJSON } from '../../../../shared/helpers.js';
|
||||
|
||||
//Import all themes
|
||||
const ThemeSnippets = {};
|
||||
@@ -40,7 +41,7 @@ const Snippetbar = createClass({
|
||||
unfoldCode : ()=>{},
|
||||
updateEditorTheme : ()=>{},
|
||||
cursorPos : {},
|
||||
snippetBundle : [],
|
||||
themeBundle : [],
|
||||
updateBrew : ()=>{}
|
||||
};
|
||||
},
|
||||
@@ -64,7 +65,10 @@ const Snippetbar = createClass({
|
||||
},
|
||||
|
||||
componentDidUpdate : async function(prevProps, prevState) {
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||
if(prevProps.renderer != this.props.renderer ||
|
||||
prevProps.theme != this.props.theme ||
|
||||
prevProps.themeBundle != this.props.themeBundle ||
|
||||
prevProps.brew.snippets != this.props.brew.snippets) {
|
||||
this.setState({
|
||||
snippets : this.compileSnippets()
|
||||
});
|
||||
@@ -97,7 +101,7 @@ const Snippetbar = createClass({
|
||||
if(key == 'snippets') {
|
||||
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
||||
return result.filter((snip)=>snip.gen || snip.subsnippets);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
compileSnippets : function() {
|
||||
@@ -105,15 +109,21 @@ const Snippetbar = createClass({
|
||||
|
||||
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
|
||||
for (let snippets of this.props.snippetBundle) {
|
||||
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||
snippets = ThemeSnippets[snippets];
|
||||
if(this.props.themeBundle.snippets) {
|
||||
for (let snippets of this.props.themeBundle.snippets) {
|
||||
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||
snippets = ThemeSnippets[snippets];
|
||||
|
||||
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||
|
||||
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
}
|
||||
}
|
||||
|
||||
const userSnippetsasJSON = brewSnippetsToJSON(this.props.brew.title || 'New Document', this.props.brew.snippets, this.props.themeBundle.snippets);
|
||||
compiledSnippets.push(userSnippetsasJSON);
|
||||
|
||||
return compiledSnippets;
|
||||
},
|
||||
|
||||
@@ -207,8 +217,6 @@ const Snippetbar = createClass({
|
||||
renderEditorButtons : function(){
|
||||
if(!this.props.showEditButtons) return;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className='editors'>
|
||||
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||
@@ -242,7 +250,6 @@ const Snippetbar = createClass({
|
||||
</div>
|
||||
</div></>}
|
||||
|
||||
|
||||
<div className='tabs'>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
onClick={()=>this.props.onViewChange('text')}>
|
||||
@@ -252,6 +259,10 @@ const Snippetbar = createClass({
|
||||
onClick={()=>this.props.onViewChange('style')}>
|
||||
<i className='fa fa-paint-brush' />
|
||||
</div>
|
||||
<div className={cx('snippet', { selected: this.props.view === 'snippet' })}
|
||||
onClick={()=>this.props.onViewChange('snippet')}>
|
||||
<i className='fas fa-th-list' />
|
||||
</div>
|
||||
<div className={cx('meta', { selected: this.props.view === 'meta' })}
|
||||
onClick={()=>this.props.onViewChange('meta')}>
|
||||
<i className='fas fa-info-circle' />
|
||||
@@ -272,11 +283,6 @@ const Snippetbar = createClass({
|
||||
|
||||
module.exports = Snippetbar;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const SnippetGroup = createClass({
|
||||
displayName : 'SnippetGroup',
|
||||
getDefaultProps : function() {
|
||||
@@ -310,7 +316,8 @@ const SnippetGroup = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='snippetGroup snippetBarButton'>
|
||||
const snippetGroup = `snippetGroup snippetBarButton ${this.props.snippets.length === 0 ? 'disabledSnippets' : ''}`;
|
||||
return <div className={snippetGroup}>
|
||||
<div className='text'>
|
||||
<i className={this.props.icon} />
|
||||
<span className='groupName'>{this.props.groupName}</span>
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
.snippets {
|
||||
display : flex;
|
||||
justify-content : flex-start;
|
||||
min-width : 327.58px;
|
||||
min-width : 432.18px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
|
||||
}
|
||||
|
||||
.editors {
|
||||
display : flex;
|
||||
justify-content : flex-end;
|
||||
min-width : 225px;
|
||||
min-width : 250px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
|
||||
|
||||
&:only-child {min-width : unset; margin-left : auto;}
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
&.meta {
|
||||
.tooltipLeft('Properties');
|
||||
}
|
||||
&.snip {
|
||||
.tooltipLeft('Snippets');
|
||||
}
|
||||
&.undo {
|
||||
.tooltipLeft('Undo');
|
||||
font-size : 0.75em;
|
||||
@@ -226,8 +229,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabledSnippets {
|
||||
color: grey;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover { background-color: #DDDDDD;}
|
||||
}
|
||||
|
||||
}
|
||||
@container editor (width < 553px) {
|
||||
@container editor (width < 683px) {
|
||||
.snippetBar {
|
||||
.editors {
|
||||
flex : 1;
|
||||
|
||||
@@ -150,6 +150,18 @@ const EditPage = createClass({
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleSnipChange : function(snippet){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style }
|
||||
@@ -443,7 +455,7 @@ const EditPage = createClass({
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
{this.renderNavbar()}
|
||||
|
||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<Editor
|
||||
@@ -451,12 +463,12 @@ const EditPage = createClass({
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onSnipChange={this.handleSnipChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
reportError={this.errorReported}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
updateBrew={this.updateBrew}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
require('./lockNotification.less');
|
||||
const React = require('react');
|
||||
import './lockNotification.less';
|
||||
import * as React from 'react';
|
||||
import request from '../../../utils/request-middleware.js';
|
||||
import Dialog from '../../../../components/dialog.jsx';
|
||||
|
||||
function LockNotification(props) {
|
||||
props = {
|
||||
shareId : 0,
|
||||
disableLock : ()=>{},
|
||||
message : '',
|
||||
shareId : 0,
|
||||
disableLock : ()=>{},
|
||||
lock : {},
|
||||
message : 'Unable to retrieve Lock Message',
|
||||
reviewRequested : false,
|
||||
...props
|
||||
};
|
||||
|
||||
const removeLock = ()=>{
|
||||
alert(`Not yet implemented - ID ${props.shareId}`);
|
||||
const [reviewState, setReviewState] = React.useState(props.reviewRequested);
|
||||
|
||||
const removeLock = async ()=>{
|
||||
await request.put(`/api/lock/review/request/${props.shareId}`)
|
||||
.then(()=>{
|
||||
setReviewState(true);
|
||||
});
|
||||
};
|
||||
|
||||
const renderReviewButton = function(){
|
||||
if(reviewState){ return <button className='inactive'>REVIEW REQUESTED</button>; };
|
||||
return <button onClick={removeLock}>REQUEST LOCK REMOVAL</button>;
|
||||
};
|
||||
|
||||
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
|
||||
@@ -19,11 +32,11 @@ function LockNotification(props) {
|
||||
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
|
||||
<hr />
|
||||
<h3>LOCK REASON</h3>
|
||||
<p>{props.message || 'Unable to retrieve Lock Message'}</p>
|
||||
<p>{props.message}</p>
|
||||
<hr />
|
||||
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
|
||||
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
|
||||
<button onClick={removeLock}>REQUEST LOCK REMOVAL</button>
|
||||
{renderReviewButton()}
|
||||
</Dialog>;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
&::backdrop { background-color : #000000AA; }
|
||||
|
||||
button {
|
||||
padding : 2px 15px;
|
||||
margin : 10px;
|
||||
color : white;
|
||||
background-color : #333333;
|
||||
|
||||
&.inactive,
|
||||
&:hover { background-color : #777777; }
|
||||
}
|
||||
|
||||
|
||||
@@ -194,13 +194,47 @@ const errorIndex = (props)=>{
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}
|
||||
|
||||
**Brew Title:** ${escape(props.brew.brewTitle)}`,
|
||||
**Brew Title:** ${escape(props.brew.brewTitle)}
|
||||
|
||||
**Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// ####### Admin page error #######
|
||||
'52' : dedent`
|
||||
## Access Denied
|
||||
You need to provide correct administrator credentials to access this page.`,
|
||||
|
||||
// ####### Lock Errors
|
||||
|
||||
'60' : dedent`Lock Error: General`,
|
||||
|
||||
'61' : dedent`Lock Get Error: Unable to get lock count`,
|
||||
|
||||
'62' : dedent`Lock Set Error: Cannot lock`,
|
||||
|
||||
'63' : dedent`Lock Set Error: Brew not found`,
|
||||
|
||||
'64' : dedent`Lock Set Error: Already locked`,
|
||||
|
||||
'65' : dedent`Lock Remove Error: Cannot unlock`,
|
||||
|
||||
'66' : dedent`Lock Remove Error: Brew not found`,
|
||||
|
||||
'67' : dedent`Lock Remove Error: Not locked`,
|
||||
|
||||
'68' : dedent`Lock Get Review Error: Cannot get review requests`,
|
||||
|
||||
'69' : dedent`Lock Set Review Error: Cannot set review request`,
|
||||
|
||||
'70' : dedent`Lock Set Review Error: Brew not found`,
|
||||
|
||||
'71' : dedent`Lock Set Review Error: Review already requested`,
|
||||
|
||||
'72' : dedent`Lock Remove Review Error: Cannot clear review request`,
|
||||
|
||||
'73' : dedent`Lock Remove Review Error: Brew not found`,
|
||||
|
||||
// ####### Other Errors
|
||||
|
||||
'90' : dedent` An unexpected error occurred while looking for these brews.
|
||||
Try again in a few minutes.`,
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ const HomePage = createClass({
|
||||
onTextChange={this.handleTextChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
showEditButtons={false}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
themeBundle={this.state.themeBundle}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
|
||||
@@ -141,6 +141,18 @@ const NewPage = createClass({
|
||||
localStorage.setItem(STYLEKEY, style);
|
||||
},
|
||||
|
||||
handleSnipChange : function(snippet){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
@@ -231,10 +243,10 @@ const NewPage = createClass({
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
onSnipChange={this.handleSnipChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
|
||||
Reference in New Issue
Block a user