mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 16:03:07 +00:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f6ba7a388 | ||
|
|
0274fb214c | ||
|
|
15519f142d | ||
|
|
6b258886a4 | ||
|
|
d23a88c997 | ||
|
|
096e17ab5a | ||
|
|
53d5f9f6e0 | ||
|
|
43b4fe75e2 | ||
|
|
816860dc4f | ||
|
|
314f758d62 | ||
|
|
c799aaa7cb | ||
|
|
2f5bc8db54 | ||
|
|
7c61a27084 | ||
|
|
ad3e83da22 | ||
|
|
8888704b58 | ||
|
|
a3dc5e78fd | ||
|
|
e5febc1fef | ||
|
|
354d01e980 | ||
|
|
63e043593a | ||
|
|
770d0c141d | ||
|
|
02c0176070 | ||
|
|
f847de852b | ||
|
|
86413b5767 | ||
|
|
747c976a14 | ||
|
|
326c28a11d | ||
|
|
263471bcbb | ||
|
|
9478454063 | ||
|
|
a9a9804517 | ||
|
|
0bde44ec2f | ||
|
|
13ad179a1b | ||
|
|
b72acd9e59 | ||
|
|
d0a1ef9571 | ||
|
|
d1f049871f | ||
|
|
070184b309 | ||
|
|
cbb41676e0 | ||
|
|
81130dd514 | ||
|
|
61d3edca17 | ||
|
|
248b56a706 | ||
|
|
90f8d1d6da | ||
|
|
8f08b71475 | ||
|
|
cc6527029c | ||
|
|
8a110567fc | ||
|
|
4e2f6b1d26 | ||
|
|
6b8db74a2b | ||
|
|
4c629772cc | ||
|
|
208593d203 | ||
|
|
99019be152 | ||
|
|
fa73e1707d | ||
|
|
d7de2e3d21 | ||
|
|
903ff4fd09 | ||
|
|
feaabacc94 | ||
|
|
bc0846c190 | ||
|
|
ecdcaadfa9 | ||
|
|
db2478f73d | ||
|
|
5d6a7e692f | ||
|
|
0fbeca1536 | ||
|
|
31d58f9075 | ||
|
|
89e6bada56 | ||
|
|
fec1766e26 | ||
|
|
f26e3d6cd1 | ||
|
|
2e6fcafc68 | ||
|
|
13b43e8902 | ||
|
|
837708fc0c | ||
|
|
2e305d5636 | ||
|
|
f9711de634 | ||
|
|
2c6779bb1c | ||
|
|
0867b142da |
72
changelog.md
72
changelog.md
@@ -7,6 +7,11 @@ h5 {
|
|||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page .taskList {
|
||||||
|
display:block;
|
||||||
|
break-inside:auto;
|
||||||
|
}
|
||||||
|
|
||||||
.taskList li input {
|
.taskList li input {
|
||||||
list-style-type : none;
|
list-style-type : none;
|
||||||
margin-left : -0.52cm;
|
margin-left : -0.52cm;
|
||||||
@@ -35,6 +40,14 @@ pre {
|
|||||||
margin-top : 0.1cm;
|
margin-top : 0.1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page ul + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page p + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
.page .openSans {
|
.page .openSans {
|
||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
@@ -44,6 +57,54 @@ pre {
|
|||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Friday 23/12/2022 - v3.5.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Only brew owners or invited authors can edit a brew
|
||||||
|
|
||||||
|
- Visiting an `/edit` page of a brew that does not list you as an author will result in an error page. Authors can be added to any brew by opening its {{fa,fa-info-circle}} **Properties** menu and typing the author's username (case-sensitive) into the **Invited Authors** bubble.
|
||||||
|
- Warn user if a newer brew version has been saved on another device
|
||||||
|
|
||||||
|
Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Monday 05/12/2022 - v3.4.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix Account page incorrect last login time
|
||||||
|
|
||||||
|
Fixes issues [#2521](https://github.com/naturalcrit/homebrewery/issues/2521)
|
||||||
|
|
||||||
|
##### Gazook
|
||||||
|
|
||||||
|
* [x] Fix crashing on iOS and Safari browsers
|
||||||
|
|
||||||
|
Fixes issues [#2531](https://github.com/naturalcrit/homebrewery/issues/2531)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Saturday 10/12/2022 - v3.4.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Fix broken tags editor
|
||||||
|
|
||||||
|
* [x] Reduce server load to fix some saving issues
|
||||||
|
|
||||||
|
Fixes issues [#2322](https://github.com/naturalcrit/homebrewery/issues/2322)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Account page help link for Google Drive errors
|
||||||
|
|
||||||
|
Fixes issues [#2520](https://github.com/naturalcrit/homebrewery/issues/2520)
|
||||||
|
}}
|
||||||
|
|
||||||
### Monday 05/12/2022 - v3.4.1
|
### Monday 05/12/2022 - v3.4.1
|
||||||
{{taskList
|
{{taskList
|
||||||
@@ -142,12 +203,7 @@ Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
|
|||||||
* [x] Fix brew settings being lost on first save
|
* [x] Fix brew settings being lost on first save
|
||||||
|
|
||||||
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
||||||
}}
|
|
||||||
|
|
||||||
\column
|
|
||||||
|
|
||||||
|
|
||||||
{{taskList
|
|
||||||
##### Gazook:
|
##### Gazook:
|
||||||
|
|
||||||
* [x] Several updates to bug reporting and error popups
|
* [x] Several updates to bug reporting and error popups
|
||||||
@@ -197,6 +253,10 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 31/08/2022 - v3.2.1
|
### Wednesday 31/08/2022 - v3.2.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -223,8 +283,6 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Saturday 27/08/2022 - v3.2.0
|
### Saturday 27/08/2022 - v3.2.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
|
|||||||
@@ -139,10 +139,10 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// Highlight injectors {style}
|
// Highlight injectors {style}
|
||||||
if(line.includes('{') && line.includes('}')){
|
if(line.includes('{') && line.includes('}')){
|
||||||
const regex = /(?<!{){(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'injection' });
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Highlight inline spans {{content}}
|
// Highlight inline spans {{content}}
|
||||||
|
|||||||
@@ -9,12 +9,18 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
const validations = require('./validations.js')
|
const validations = require('./validations.js');
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||||
|
|
||||||
const homebreweryThumbnail = require('../../thumbnail.png');
|
const homebreweryThumbnail = require('../../thumbnail.png');
|
||||||
|
|
||||||
|
const callIfExists = (val, fn, ...args)=>{
|
||||||
|
if(val[fn]) {
|
||||||
|
val[fn](...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MetadataEditor = createClass({
|
const MetadataEditor = createClass({
|
||||||
displayName : 'MetadataEditor',
|
displayName : 'MetadataEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -53,28 +59,25 @@ const MetadataEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleFieldChange : function(name, e){
|
handleFieldChange : function(name, e){
|
||||||
e.persist();
|
|
||||||
|
|
||||||
// load validation rules, and check input value against them
|
// load validation rules, and check input value against them
|
||||||
const inputRules = validations[name] ?? [];
|
const inputRules = validations[name] ?? [];
|
||||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||||
|
|
||||||
// if no validation rules, save to props
|
// if no validation rules, save to props
|
||||||
if(validationErr.length === 0){
|
if(validationErr.length === 0){
|
||||||
e.target.setCustomValidity('');
|
callIfExists(e.target, 'setCustomValidity', '');
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.metadata,
|
...this.props.metadata,
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// if validation issues, display built-in browser error popup with each error.
|
// if validation issues, display built-in browser error popup with each error.
|
||||||
console.log(validationErr);
|
|
||||||
const errMessage = validationErr.map((err)=>{
|
const errMessage = validationErr.map((err)=>{
|
||||||
return `- ${err}`;
|
return `- ${err}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
e.target.setCustomValidity(errMessage);
|
callIfExists(e.target, 'setCustomValidity', errMessage);
|
||||||
e.target.reportValidity();
|
callIfExists(e.target, 'reportValidity');
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSystem : function(system, e){
|
handleSystem : function(system, e){
|
||||||
@@ -247,6 +250,8 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='metadataEditor'>
|
return <div className='metadataEditor'>
|
||||||
|
<h1 className='sectionHead'>Brew</h1>
|
||||||
|
|
||||||
<div className='field title'>
|
<div className='field title'>
|
||||||
<label>title</label>
|
<label>title</label>
|
||||||
<input type='text' className='value'
|
<input type='text' className='value'
|
||||||
@@ -280,8 +285,6 @@ const MetadataEditor = createClass({
|
|||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||||
|
|
||||||
{this.renderAuthors()}
|
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -293,6 +296,23 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
{this.renderRenderOptions()}
|
{this.renderRenderOptions()}
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<h1 className='sectionHead'>Authors</h1>
|
||||||
|
|
||||||
|
{this.renderAuthors()}
|
||||||
|
|
||||||
|
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]}
|
||||||
|
validators={[(v)=>!this.props.metadata.authors.includes(v)]}
|
||||||
|
placeholder='invite author' unique={true}
|
||||||
|
values={this.props.metadata.invitedAuthors}
|
||||||
|
notes={['Invited authors are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
||||||
|
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<h1 className='sectionHead'>Privacy</h1>
|
||||||
|
|
||||||
<div className='field publish'>
|
<div className='field publish'>
|
||||||
<label>publish</label>
|
<label>publish</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
|
|||||||
@@ -10,6 +10,15 @@
|
|||||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||||
overflow-y : auto;
|
overflow-y : auto;
|
||||||
|
|
||||||
|
.sectionHead {
|
||||||
|
font-weight: 1000;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@@ -30,6 +39,7 @@
|
|||||||
}
|
}
|
||||||
.field{
|
.field{
|
||||||
display : flex;
|
display : flex;
|
||||||
|
flex-wrap : wrap;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
min-width : 200px;
|
min-width : 200px;
|
||||||
&>label{
|
&>label{
|
||||||
@@ -78,6 +88,11 @@
|
|||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size : 0.6em;
|
||||||
|
font-style : italic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -128,10 +143,6 @@
|
|||||||
button.unpublish{
|
button.unpublish{
|
||||||
.button(@silver);
|
.button(@silver);
|
||||||
}
|
}
|
||||||
small{
|
|
||||||
font-size : 0.6em;
|
|
||||||
font-style : italic;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete.field .value{
|
.delete.field .value{
|
||||||
@@ -196,6 +207,7 @@
|
|||||||
}
|
}
|
||||||
.field .list {
|
.field .list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ const StringArrayEditor = createClass({
|
|||||||
label : '',
|
label : '',
|
||||||
values : [],
|
values : [],
|
||||||
valuePatterns : null,
|
valuePatterns : null,
|
||||||
|
validators : [],
|
||||||
placeholder : '',
|
placeholder : '',
|
||||||
|
notes : [],
|
||||||
unique : false,
|
unique : false,
|
||||||
cannotEdit : [],
|
cannotEdit : [],
|
||||||
onChange : ()=>{}
|
onChange : ()=>{}
|
||||||
@@ -83,7 +85,8 @@ const StringArrayEditor = createClass({
|
|||||||
}
|
}
|
||||||
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
||||||
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
||||||
return matchesPatterns && uniqueIfSet;
|
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
|
||||||
|
return matchesPatterns && uniqueIfSet && passesValidators;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleValueInputKeyDown : function(event, index) {
|
handleValueInputKeyDown : function(event, index) {
|
||||||
@@ -123,17 +126,21 @@ const StringArrayEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return <div className='field values'>
|
return <div className='field'>
|
||||||
<label>{this.props.label}</label>
|
<label>{this.props.label}</label>
|
||||||
<div className='list'>
|
<div style={{ flex: '1 0' }}>
|
||||||
{valueElements}
|
<div className='list'>
|
||||||
<div className='input-group'>
|
{valueElements}
|
||||||
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
<div className='input-group'>
|
||||||
value={this.state.temporaryValue}
|
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
value={this.state.temporaryValue}
|
||||||
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||||
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||||
|
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this.props.notes ? this.props.notes.map((n)=><p><small>{n}</small></p>) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const AccountPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderUiItems : function() {
|
renderUiItems : function() {
|
||||||
// console.log(this.props.uiItems);
|
|
||||||
return <>
|
return <>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h1>Account Information <i className='fas fa-user'></i></h1>
|
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||||
@@ -51,12 +50,16 @@ const AccountPage = createClass({
|
|||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||||
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount || '-'}</p>
|
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||||
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
||||||
{this.props.uiItems.googleId ? <p><strong>Brews on Google Drive: </strong> {this.props.uiItems.fileCount || '-'}</p> : '' }
|
{this.props.uiItems.googleId &&
|
||||||
|
<p>
|
||||||
|
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -230,7 +230,8 @@ const EditPage = createClass({
|
|||||||
brew : { ...prevState.brew,
|
brew : { ...prevState.brew,
|
||||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
||||||
editId : this.savedBrew.editId,
|
editId : this.savedBrew.editId,
|
||||||
shareId : this.savedBrew.shareId
|
shareId : this.savedBrew.shareId,
|
||||||
|
version : this.savedBrew.version
|
||||||
},
|
},
|
||||||
isPending : false,
|
isPending : false,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
@@ -329,6 +330,16 @@ const EditPage = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.state.errors.response.error.status === 409) {
|
||||||
|
const message = this.state.errors.response.body?.message;
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer'>
|
||||||
|
{message ? message : 'Conflict: please refresh to get latest changes'}
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
|
|||||||
2129
package-lock.json
generated
2129
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.4.1",
|
"version": "3.5.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.11.x"
|
"node": "16.11.x"
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.20.5",
|
"@babel/core": "^7.20.7",
|
||||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||||
"@babel/preset-env": "^7.19.4",
|
"@babel/preset-env": "^7.19.4",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
@@ -65,31 +65,31 @@
|
|||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "11.1.0",
|
"fs-extra": "11.1.0",
|
||||||
"googleapis": "109.0.1",
|
"googleapis": "110.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "4.2.3",
|
"marked": "4.2.4",
|
||||||
"marked-extended-tables": "^1.0.5",
|
"marked-extended-tables": "^1.0.5",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongoose": "^6.7.5",
|
"mongoose": "^6.8.1",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.0",
|
"nconf": "^0.12.0",
|
||||||
"npm": "^8.10.0",
|
"npm": "^8.10.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-frame-component": "4.1.3",
|
"react-frame-component": "4.1.3",
|
||||||
"react-router-dom": "6.4.4",
|
"react-router-dom": "6.6.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.30.0",
|
||||||
"eslint-plugin-react": "^7.31.11",
|
"eslint-plugin-react": "^7.31.11",
|
||||||
"jest": "^29.2.2",
|
"jest": "^29.2.2",
|
||||||
"supertest": "^6.3.1"
|
"supertest": "^6.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,7 +280,6 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
|||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
|
|
||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -341,7 +340,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
data.title = 'Account Information Page';
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
let auth;
|
let auth;
|
||||||
let files;
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
try {
|
try {
|
||||||
@@ -353,9 +352,9 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
if(auth.credentials.access_token) {
|
if(auth.credentials.access_token) {
|
||||||
try {
|
try {
|
||||||
files = await GoogleActions.listGoogleBrews(auth);
|
googleCount = await GoogleActions.listGoogleBrews(auth);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
files = undefined;
|
googleCount = undefined;
|
||||||
console.log('List Google files failed!');
|
console.log('List Google files failed!');
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
@@ -363,18 +362,19 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||||
const brews = await HomebrewModel.find(query, 'id')
|
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
|
mongoCount = 0;
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
data.uiItems = {
|
data.uiItems = {
|
||||||
username : req.account.username,
|
username : req.account.username,
|
||||||
issued : req.account.issued,
|
issued : req.account.issued,
|
||||||
mongoCount : brews.length,
|
googleId : Boolean(req.account.googleId),
|
||||||
googleId : Boolean(req.account.googleId),
|
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
||||||
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
mongoCount : mongoCount,
|
||||||
fileCount : files?.length || '-'
|
googleCount : googleCount?.length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,6 @@ const GoogleActions = {
|
|||||||
text : file.data,
|
text : file.data,
|
||||||
|
|
||||||
description : obj.data.description,
|
description : obj.data.description,
|
||||||
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
|
|
||||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||||
authors : [],
|
authors : [],
|
||||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const getId = (req)=>{
|
|||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBrew = (accessType)=>{
|
const getBrew = (accessType, stubOnly = false)=>{
|
||||||
// Create middleware with the accessType passed in as part of the scope
|
// Create middleware with the accessType passed in as part of the scope
|
||||||
return async (req, res, next)=>{
|
return async (req, res, next)=>{
|
||||||
// Get relevant IDs for the brew
|
// Get relevant IDs for the brew
|
||||||
@@ -45,7 +45,7 @@ const getBrew = (accessType)=>{
|
|||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there is a google id, try to find the google brew
|
||||||
if(googleId || stub?.googleId) {
|
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||||
let googleError;
|
let googleError;
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -57,16 +57,24 @@ const getBrew = (accessType)=>{
|
|||||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||||
stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew;
|
stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew;
|
||||||
}
|
}
|
||||||
|
const authorsExist = stub?.authors?.length > 0;
|
||||||
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
|
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||||
|
throw `The current logged in user does not have editor access to this brew.
|
||||||
|
|
||||||
|
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
|
||||||
|
}
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
if(!stub) {
|
if(!stub && !stubOnly) {
|
||||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof stub.tags === 'string') {
|
if(typeof stub?.tags === 'string') {
|
||||||
stub.tags = [];
|
stub.tags = [];
|
||||||
}
|
}
|
||||||
req.brew = stub;
|
req.brew = stub || {};
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
@@ -108,7 +116,7 @@ const excludePropsFromUpdate = (brew)=>{
|
|||||||
|
|
||||||
const excludeGoogleProps = (brew)=>{
|
const excludeGoogleProps = (brew)=>{
|
||||||
const modified = _.clone(brew);
|
const modified = _.clone(brew);
|
||||||
const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
delete modified[prop];
|
delete modified[prop];
|
||||||
}
|
}
|
||||||
@@ -116,7 +124,7 @@ const excludeGoogleProps = (brew)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const excludeStubProps = (brew)=>{
|
const excludeStubProps = (brew)=>{
|
||||||
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version'];
|
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
brew[prop] = undefined;
|
brew[prop] = undefined;
|
||||||
}
|
}
|
||||||
@@ -184,7 +192,13 @@ const newBrew = async (req, res)=>{
|
|||||||
|
|
||||||
const updateBrew = async (req, res)=>{
|
const updateBrew = async (req, res)=>{
|
||||||
// Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method
|
// Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method
|
||||||
let brew = _.assign(req.brew, excludePropsFromUpdate(req.body));
|
const brewFromClient = excludePropsFromUpdate(req.body);
|
||||||
|
if(req.brew.version > brewFromClient.version) {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let brew = _.assign(req.brew, brewFromClient);
|
||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
const googleId = brew.googleId;
|
const googleId = brew.googleId;
|
||||||
let afterSave = async ()=>true;
|
let afterSave = async ()=>true;
|
||||||
@@ -230,28 +244,29 @@ const updateBrew = async (req, res)=>{
|
|||||||
brew.text = undefined;
|
brew.text = undefined;
|
||||||
}
|
}
|
||||||
brew.updatedAt = new Date();
|
brew.updatedAt = new Date();
|
||||||
|
brew.version += 1;
|
||||||
|
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||||
|
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the brew from the database again (if it existed there to begin with), and assign the existing brew to it
|
// define a function to catch our save errors
|
||||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
const saveError = (err)=>{
|
||||||
|
console.error(err);
|
||||||
if(!brew.markModified) {
|
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||||
// If it wasn't in the database, create a new db brew
|
};
|
||||||
brew = new HomebrewModel(brew);
|
let saved;
|
||||||
|
if(!brew._id) {
|
||||||
|
// if the brew does not have a stub id, create and save it, then write the new value back to the brew.
|
||||||
|
saved = await new HomebrewModel(brew).save().catch(saveError);
|
||||||
|
brew = saved?.toObject();
|
||||||
|
} else {
|
||||||
|
// if the brew does have a stub id, update it using the stub id as the key.
|
||||||
|
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||||
|
saved = await brew.save()
|
||||||
|
.catch(saveError);
|
||||||
}
|
}
|
||||||
|
|
||||||
brew.markModified('authors');
|
|
||||||
brew.markModified('systems');
|
|
||||||
|
|
||||||
// Save the database brew
|
|
||||||
const saved = await brew.save()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
|
||||||
});
|
|
||||||
if(!saved) return;
|
if(!saved) return;
|
||||||
// Call and wait for afterSave to complete
|
// Call and wait for afterSave to complete
|
||||||
const after = await afterSave();
|
const after = await afterSave();
|
||||||
@@ -327,8 +342,8 @@ const deleteBrew = async (req, res, next)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', asyncHandler(newBrew));
|
router.post('/api', asyncHandler(newBrew));
|
||||||
router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
router.put('/api/:id', asyncHandler(getBrew('edit', true)), asyncHandler(updateBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
router.put('/api/update/:id', asyncHandler(getBrew('edit', true)), asyncHandler(updateBrew));
|
||||||
router.delete('/api/:id', asyncHandler(deleteBrew));
|
router.delete('/api/:id', asyncHandler(deleteBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
textBin : { type: Buffer },
|
textBin : { type: Buffer },
|
||||||
pageCount : { type: Number, default: 1 },
|
pageCount : { type: Number, default: 1 },
|
||||||
|
|
||||||
description : { type: String, default: '' },
|
description : { type: String, default: '' },
|
||||||
tags : [String],
|
tags : [String],
|
||||||
systems : [String],
|
systems : [String],
|
||||||
renderer : { type: String, default: '' },
|
renderer : { type: String, default: '' },
|
||||||
authors : [String],
|
authors : [String],
|
||||||
published : { type: Boolean, default: false },
|
invitedAuthors : [String],
|
||||||
thumbnail : { type: String, default: '' },
|
published : { type: Boolean, default: false },
|
||||||
|
thumbnail : { type: String, default: '' },
|
||||||
|
|
||||||
createdAt : { type: Date, default: Date.now },
|
createdAt : { type: Date, default: Date.now },
|
||||||
updatedAt : { type: Date, default: Date.now },
|
updatedAt : { type: Date, default: Date.now },
|
||||||
|
|||||||
Reference in New Issue
Block a user