mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-11-14 03:58:43 +00:00
Merge branch 'master' into clear_vite_cache
This commit is contained in:
+22
-1
@@ -90,9 +90,30 @@ pre {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Tuesday 10/20/2026 - v3.24.2
|
||||
{{taskList
|
||||
##### G-Ambatte
|
||||
* [x] Hotfix for memory leak on server
|
||||
|
||||
Fixes issue [#5090](https://github.com/naturalcrit/homebrewery/issues/5090)
|
||||
|
||||
##### Calculuschild
|
||||
* [x] Fix brew variables not working across pages
|
||||
|
||||
* [x] Allow brew variable definitions to be on adjacent lines (no blank line required anymore)
|
||||
|
||||
##### 5e-Cleric
|
||||
* [x] Partial rollout of some site UI changes (includes alternate views and darkmode in {{openSans **BREWS** :fas_beer_mug_empty:}} page, and changes to {{openSans **VAULT** :fas_dungeon:}})
|
||||
|
||||
##### Abquintic, Calculuschild, 5e-Cleric
|
||||
* [x] Multiple background fixes and code cleanups
|
||||
}}
|
||||
|
||||
|
||||
### Tuesday 9/29/2026 - v3.24.1
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
@@ -169,7 +190,7 @@ Fixes issue [#3840](https://github.com/naturalcrit/homebrewery/issues/3840)
|
||||
|
||||
Fixes issue [#2037](https://github.com/naturalcrit/homebrewery/issues/2037)
|
||||
|
||||
* [x] Remember editor theme across tabs
|
||||
* [x] Remember editor theme across tabs, and new {{openSans **SETTINGS :fas_gear:**}} tab with editor settings
|
||||
|
||||
Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858)
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* eslint-disable max-lines */
|
||||
import '../uiEditor.less';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import React, { useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Combobox from '@components/combobox.jsx';
|
||||
import TagInput from '../tagInput/tagInput.jsx';
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Themes from '@themes/themes.json';
|
||||
import validations from './validations.js';
|
||||
|
||||
import Combobox from '@components/combobox.jsx';
|
||||
import TagInput from '../tagInput/tagInput.jsx';
|
||||
|
||||
import homebreweryThumbnail from '../../thumbnail.png';
|
||||
|
||||
const callIfExists = (val, fn, ...args)=>{
|
||||
@@ -18,48 +18,28 @@ const callIfExists = (val, fn, ...args)=>{
|
||||
}
|
||||
};
|
||||
|
||||
const MetadataEditor = createReactClass({
|
||||
displayName : 'MetadataEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
metadata : {
|
||||
editId : null,
|
||||
shareId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
thumbnail : '',
|
||||
tags : [],
|
||||
published : false,
|
||||
authors : [],
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB',
|
||||
lang : 'en'
|
||||
},
|
||||
|
||||
onChange : ()=>{},
|
||||
reportError : ()=>{}
|
||||
};
|
||||
const MetadataEditor = ({
|
||||
metadata = {
|
||||
shareId : null,
|
||||
editId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
tags : [],
|
||||
lang : 'en',
|
||||
renderer : 'legacy',
|
||||
authors : [],
|
||||
published : false,
|
||||
thumbnail : '',
|
||||
theme : '5ePHB',
|
||||
},
|
||||
onChange = ()=>{},
|
||||
reportError = ()=>{},
|
||||
userThemes = {},
|
||||
themeBundle = {}
|
||||
})=>{
|
||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||
|
||||
getInitialState : function(){
|
||||
return {
|
||||
isOwner : global.account?.username && global.account?.username === this.props.metadata?.authors[0],
|
||||
showThumbnail : true
|
||||
};
|
||||
},
|
||||
|
||||
toggleThumbnailDisplay : function(){
|
||||
this.setState({
|
||||
showThumbnail : !this.state.showThumbnail
|
||||
});
|
||||
},
|
||||
|
||||
renderThumbnail : function(){
|
||||
if(!this.state.showThumbnail) return;
|
||||
return <img className='thumbnail-preview' src={this.props.metadata.thumbnail || homebreweryThumbnail}></img>;
|
||||
},
|
||||
|
||||
handleFieldChange : function(name, e){
|
||||
const handleFieldChange = (name, e)=>{
|
||||
// load validation rules, and check input value against them
|
||||
const inputRules = validations[name] ?? [];
|
||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||
@@ -72,8 +52,8 @@ const MetadataEditor = createReactClass({
|
||||
// if no validation rules, save to props
|
||||
if(validationErr.length === 0){
|
||||
callIfExists(e.target, 'setCustomValidity', '');
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
onChange({
|
||||
...metadata,
|
||||
[name] : e.target.value
|
||||
});
|
||||
return true;
|
||||
@@ -86,45 +66,37 @@ const MetadataEditor = createReactClass({
|
||||
debouncedReportValidity(e.target, errMessage);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
handleRenderer : function(renderer, e){
|
||||
};
|
||||
const handleRenderer = (renderer, e)=>{
|
||||
if(e.target.checked){
|
||||
this.props.metadata.renderer = renderer;
|
||||
metadata.renderer = renderer;
|
||||
if(renderer == 'legacy')
|
||||
this.props.metadata.theme = '5ePHB';
|
||||
metadata.theme = '5ePHB';
|
||||
}
|
||||
this.props.onChange(this.props.metadata, 'renderer');
|
||||
},
|
||||
|
||||
handlePublish : function(val){
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
onChange(metadata, 'renderer');
|
||||
};
|
||||
const handlePublish = (val)=>{
|
||||
onChange({
|
||||
...metadata,
|
||||
published : val
|
||||
});
|
||||
},
|
||||
|
||||
handleTheme : function(theme){
|
||||
this.props.metadata.renderer = theme.renderer;
|
||||
this.props.metadata.theme = theme.path;
|
||||
|
||||
this.props.onChange(this.props.metadata, 'theme');
|
||||
},
|
||||
|
||||
handleThemeWritein : function(e) {
|
||||
};
|
||||
const handleTheme = (theme)=>{
|
||||
metadata.renderer = theme.renderer;
|
||||
metadata.theme = theme.path;
|
||||
onChange(metadata, 'theme');
|
||||
};
|
||||
const handleThemeWritein = (e)=>{
|
||||
const shareId = e.target.value.split('/').pop(); //Extract just the ID if a URL was pasted in
|
||||
this.props.metadata.theme = shareId;
|
||||
|
||||
this.props.onChange(this.props.metadata, 'theme');
|
||||
},
|
||||
|
||||
handleLanguage : function(languageCode){
|
||||
this.props.metadata.lang = languageCode;
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
|
||||
handleDelete : function(){
|
||||
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
|
||||
metadata.theme = shareId;
|
||||
onChange(metadata, 'theme');
|
||||
};
|
||||
const handleLanguage = (languageCode)=>{
|
||||
metadata.lang = languageCode;
|
||||
onChange(metadata);
|
||||
};
|
||||
const handleDelete = ()=>{
|
||||
if(metadata.authors && metadata.authors.length <= 1){
|
||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||
} else {
|
||||
@@ -132,54 +104,39 @@ const MetadataEditor = createReactClass({
|
||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||
}
|
||||
|
||||
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
||||
request.delete(`/api/${metadata.googleId ?? ''}${metadata.editId}`)
|
||||
.send()
|
||||
.end((err, res)=>{
|
||||
if(err) {
|
||||
this.props.reportError(err);
|
||||
reportError(err);
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleDeleteAuthor : function(author){
|
||||
};
|
||||
const handleDeleteAuthor = (author)=>{
|
||||
if(!confirm('Are you sure you want to remove this author? They will lose all edit access to this brew, and it will dissapear from their userpage.')) return;
|
||||
if(!this.props.metadata.authors.includes(author)) return;
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
authors : this.props.metadata.authors.filter((a)=>a !== author)
|
||||
if(!metadata.authors.includes(author)) return;
|
||||
onChange({
|
||||
...metadata,
|
||||
authors : metadata.authors.filter((a)=>a !== author)
|
||||
});
|
||||
},
|
||||
|
||||
renderPublish : function(){
|
||||
if(this.props.metadata.published){
|
||||
return <button id='publish-button' className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||
};
|
||||
const renderPublish = ()=>{
|
||||
if(metadata.published){
|
||||
return <button id='publish-button' className='unpublish' onClick={()=>handlePublish(false)}>
|
||||
<i className='fas fa-ban' aria-hidden='true' /> unpublish
|
||||
</button>;
|
||||
} else {
|
||||
return <button id='publish-button' className='publish' onClick={()=>this.handlePublish(true)}>
|
||||
return <button id='publish-button' className='publish' onClick={()=>handlePublish(true)}>
|
||||
<i className='fas fa-globe' aria-hidden='true' /> publish
|
||||
</button>;
|
||||
}
|
||||
},
|
||||
|
||||
renderDelete : function(){
|
||||
if(!this.props.metadata.editId) return;
|
||||
|
||||
return <div className='field delete'>
|
||||
<label htmlFor='delete-button'>delete</label>
|
||||
<div className='value'>
|
||||
<button id='delete-button' onClick={this.handleDelete}>
|
||||
<i className='fas fa-trash-alt' /> delete brew
|
||||
</button>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderAuthors : function(){
|
||||
const authors = this.props.metadata.authors;
|
||||
if(!this.state.isOwner || authors.length < 2) return (
|
||||
};
|
||||
const renderAuthors = ()=>{
|
||||
const authors = metadata.authors;
|
||||
const isOwner = global.account?.username && global.account?.username === authors[0];
|
||||
if(!isOwner || authors.length < 2) return (
|
||||
<div className='field authors'>
|
||||
<label>authors</label>
|
||||
<div className='value'>
|
||||
@@ -214,7 +171,7 @@ const MetadataEditor = createReactClass({
|
||||
{author}
|
||||
</a>
|
||||
<button
|
||||
onClick={()=>this.handleDeleteAuthor(author)}
|
||||
onClick={()=>handleDeleteAuthor(author)}
|
||||
className='delete'
|
||||
title={`Remove ${author} as an author`}
|
||||
>
|
||||
@@ -225,14 +182,13 @@ const MetadataEditor = createReactClass({
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderThemeDropdown : function(){
|
||||
const mergedThemes = _.merge(Themes, this.props.userThemes);
|
||||
};
|
||||
const renderThemeDropdown = ()=>{
|
||||
const mergedThemes = _.merge(Themes, userThemes);
|
||||
|
||||
const listThemes = (renderer)=>{
|
||||
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
|
||||
if(theme.path == this.props.metadata.shareId) return;
|
||||
if(theme.path == metadata.shareId) return;
|
||||
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||
return <div className='item' key={`${renderer}_${theme.name}`} value={`${theme.author ?? renderer} : ${theme.name}`} data={theme} title={''}>
|
||||
@@ -248,8 +204,8 @@ const MetadataEditor = createReactClass({
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
const currentRenderer = this.props.metadata.renderer;
|
||||
const currentThemeDisplay = this.props.themeBundle?.name ? `${this.props.themeBundle.author ?? currentRenderer} : ${this.props.themeBundle.name}` : 'No Theme Selected';
|
||||
const currentRenderer = metadata.renderer;
|
||||
const currentThemeDisplay = themeBundle?.name ? `${themeBundle.author ?? currentRenderer} : ${themeBundle.name}` : 'No Theme Selected';
|
||||
let dropdown;
|
||||
|
||||
if(currentRenderer == 'legacy') {
|
||||
@@ -265,11 +221,11 @@ const MetadataEditor = createReactClass({
|
||||
className='themes-dropdown'
|
||||
default={currentThemeDisplay}
|
||||
placeholder='Select from below, or enter the Share URL or ID of a brew with the meta:theme tag'
|
||||
onSelect={(value)=>this.handleTheme(value)}
|
||||
onSelect={(value)=>handleTheme(value)}
|
||||
onEntry={(e)=>{
|
||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||
if(this.handleFieldChange('theme', e))
|
||||
this.handleThemeWritein(e);
|
||||
if(handleFieldChange('theme', e))
|
||||
handleThemeWritein(e);
|
||||
}}
|
||||
options={listThemes(currentRenderer)}
|
||||
autoSuggest={{
|
||||
@@ -285,9 +241,8 @@ const MetadataEditor = createReactClass({
|
||||
<label htmlFor='combobox-themes'>theme</label>
|
||||
{dropdown}
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderLanguageDropdown : function(){
|
||||
};
|
||||
const renderLanguageDropdown = ()=>{
|
||||
const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant'];
|
||||
const listLanguages = ()=>{
|
||||
return _.map(langCodes.sort(), (code, index)=>{
|
||||
@@ -306,12 +261,12 @@ const MetadataEditor = createReactClass({
|
||||
<Combobox trigger='click'
|
||||
id='combobox-language'
|
||||
className='language-dropdown'
|
||||
default={this.props.metadata.lang || ''}
|
||||
default={metadata.lang || ''}
|
||||
placeholder='en'
|
||||
onSelect={(value)=>this.handleLanguage(value)}
|
||||
onSelect={(value)=>handleLanguage(value)}
|
||||
onEntry={(e)=>{
|
||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||
this.handleFieldChange('lang', e);
|
||||
handleFieldChange('lang', e);
|
||||
}}
|
||||
options={listLanguages()}
|
||||
autoSuggest={{
|
||||
@@ -321,11 +276,9 @@ const MetadataEditor = createReactClass({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderRenderOptions : function(){
|
||||
};
|
||||
const renderRenderOptions = ()=>{
|
||||
return <div className='field renderers'>
|
||||
<label>Renderer</label>
|
||||
<div className='value'>
|
||||
@@ -334,8 +287,8 @@ const MetadataEditor = createReactClass({
|
||||
type='radio'
|
||||
value = 'legacy'
|
||||
name = 'renderer'
|
||||
checked={this.props.metadata.renderer === 'legacy'}
|
||||
onChange={(e)=>this.handleRenderer('legacy', e)} />
|
||||
checked={metadata.renderer === 'legacy'}
|
||||
onChange={(e)=>handleRenderer('legacy', e)} />
|
||||
Legacy
|
||||
</label>
|
||||
|
||||
@@ -344,74 +297,81 @@ const MetadataEditor = createReactClass({
|
||||
type='radio'
|
||||
value = 'V3'
|
||||
name = 'renderer'
|
||||
checked={this.props.metadata.renderer === 'V3'}
|
||||
onChange={(e)=>this.handleRenderer('V3', e)} />
|
||||
checked={metadata.renderer === 'V3'}
|
||||
onChange={(e)=>handleRenderer('V3', e)} />
|
||||
V3
|
||||
</label>
|
||||
<small><a href='/legacy' target='_blank' rel='noopener noreferrer'>Click here to see the demo page for the old Legacy renderer!</a></small>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
};
|
||||
|
||||
render : function(){
|
||||
return <div className='metadataEditor uiEditor'>
|
||||
return (
|
||||
<div className='metadataEditor uiEditor'>
|
||||
<h1>Properties Editor</h1>
|
||||
|
||||
<div className='field title'>
|
||||
<label htmlFor='title_field'>title</label>
|
||||
<input type='text' id='title_field' className='value'
|
||||
defaultValue={this.props.metadata.title}
|
||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||
<input
|
||||
type='text'
|
||||
id='title_field'
|
||||
className='value'
|
||||
defaultValue={metadata.title}
|
||||
onChange={(e)=>handleFieldChange('title', e)}
|
||||
/>
|
||||
</div>
|
||||
<fieldset className='field-group'>
|
||||
<div className='field-column'>
|
||||
<div className='field description'>
|
||||
<label htmlFor='description_field'>description</label>
|
||||
<textarea id='description_field' defaultValue={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
<textarea
|
||||
id='description_field'
|
||||
defaultValue={metadata.description}
|
||||
className='value'
|
||||
onChange={(e)=>handleFieldChange('description', e)}
|
||||
/>
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label htmlFor='thumbnail_field'>thumbnail</label>
|
||||
<input type='text'
|
||||
<input
|
||||
type='text'
|
||||
id='thumbnail_field'
|
||||
defaultValue={this.props.metadata.thumbnail}
|
||||
defaultValue={metadata.thumbnail}
|
||||
placeholder='https://my.thumbnail.url'
|
||||
className='value'
|
||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||
<button className='display' onClick={this.toggleThumbnailDisplay}
|
||||
aria-label={`${this.state.showThumbnail ? 'hide thumbnail' : 'show thumbnail'}`}>
|
||||
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||
onChange={(e)=>handleFieldChange('thumbnail', e)}
|
||||
/>
|
||||
<button
|
||||
className='display'
|
||||
onClick={()=>setShowThumbnail(!showThumbnail)}
|
||||
aria-label={`${showThumbnail ? 'hide thumbnail' : 'show thumbnail'}`}>
|
||||
<i className={`fas fa-caret-${showThumbnail ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderThumbnail()}
|
||||
{showThumbnail && <img className='thumbnail-preview' src={metadata.thumbnail || homebreweryThumbnail}></img>}
|
||||
</fieldset>
|
||||
|
||||
<div className='field tags'>
|
||||
<label htmlFor='combobox-tags'>Tags</label>
|
||||
<div className='value' >
|
||||
<div className='value'>
|
||||
<TagInput
|
||||
id='combobox-tags'
|
||||
label='tags'
|
||||
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
|
||||
placeholder='add tag' unique={true}
|
||||
values={this.props.metadata.tags}
|
||||
onChange={(e)=>this.handleFieldChange('tags', e)}
|
||||
placeholder='add tag'
|
||||
unique={true}
|
||||
values={metadata.tags}
|
||||
onChange={(e)=>handleFieldChange('tags', e)}
|
||||
tooltip='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderLanguageDropdown()}
|
||||
|
||||
{this.renderThemeDropdown()}
|
||||
|
||||
{this.renderRenderOptions()}
|
||||
{renderLanguageDropdown()}
|
||||
{renderThemeDropdown()}
|
||||
{renderRenderOptions()}
|
||||
|
||||
<h2>Authors</h2>
|
||||
|
||||
{this.renderAuthors()}
|
||||
|
||||
{renderAuthors()}
|
||||
<div className='field invitedAuthors'>
|
||||
<label htmlFor='combobox-invited-authors'>Invited authors</label>
|
||||
<div className='value'>
|
||||
@@ -419,30 +379,39 @@ const MetadataEditor = createReactClass({
|
||||
id='combobox-invited-authors'
|
||||
label='invited authors'
|
||||
valuePatterns={/.+/}
|
||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||
placeholder='invite author' unique={true}
|
||||
validators={[(v)=>!metadata.authors?.includes(v)]}
|
||||
placeholder='invite author'
|
||||
unique={true}
|
||||
tooltip={`Invited author usernames are case sensitive.
|
||||
After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.`}
|
||||
values={this.props.metadata.invitedAuthors}
|
||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
||||
values={metadata.invitedAuthors}
|
||||
onChange={(e)=>handleFieldChange('invitedAuthors', e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Privacy</h2>
|
||||
|
||||
<div className='field publish'>
|
||||
<label htmlFor='publish-button'>publish</label>
|
||||
<div className='value'>
|
||||
{this.renderPublish()}
|
||||
<small>Published brews are searchable in <a href='/vault'>the Vault</a> and visible on your user page. Unpublished brews are not indexed in the Vault or visible on your user page, but can still be shared and indexed by search engines. You can unpublish a brew any time.</small>
|
||||
{renderPublish()}
|
||||
<small>
|
||||
Published brews are searchable in <a href='/vault'>the Vault</a> and visible on your user page. Unpublished brews are not
|
||||
indexed in the Vault or visible on your user page, but can still be shared and indexed by search engines. You can unpublish a
|
||||
brew any time.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderDelete()}
|
||||
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
{metadata.editId && <div className='field delete'>
|
||||
<label htmlFor='delete-button'>delete</label>
|
||||
<div className='value'>
|
||||
<button id='delete-button' onClick={handleDelete}>
|
||||
<i className='fas fa-trash-alt' /> delete brew
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetadataEditor;
|
||||
|
||||
Generated
+22
-22
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"version": "3.24.1",
|
||||
"version": "3.24.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "homebrewery",
|
||||
"version": "3.24.1",
|
||||
"version": "3.24.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -49,7 +49,7 @@
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^4.9.1",
|
||||
"lodash": "^4.18.1",
|
||||
"marked-hbfm": "^1.0.1",
|
||||
"marked-hbfm": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.31.0",
|
||||
"mongoose": "^9.11.2",
|
||||
@@ -11538,9 +11538,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked-emoji": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.3.tgz",
|
||||
"integrity": "sha512-fChW/AfUqCHgoEC1nFDgiw3OR/qsi71/QXH/HTo05yd6B5+T+VHh1SqCpn/HpeGLDxkA+MK4+hr4eULB2/A8Jw==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-3.0.0.tgz",
|
||||
"integrity": "sha512-4IODef6jiQfuRsWOXSH/q9YiXmpVemvm1raj4WmObkWeAPg5bMMPTvbDpxbn4BIvbM245a1Dhq6RAzCS0WFGOw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"marked": ">=4 <19"
|
||||
@@ -11568,25 +11568,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked-hbfm": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked-hbfm/-/marked-hbfm-1.0.1.tgz",
|
||||
"integrity": "sha512-mqz1ALIk0POeRCGjAGDTRhyKrMe+i5ycsxLV/8MlG13xC/oTEXA66VHZvIMNLotdM8jNNnLC9TFLCWECN4QHug==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked-hbfm/-/marked-hbfm-1.0.3.tgz",
|
||||
"integrity": "sha512-j+TLQqHv1PGb/ypxh8r+kGlgto1t3373GxxDn1gTBk473QC85uV4PteZfLLq8U5H2OLODbbyEGzxVvYdnOT0lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-plugin-jest": "^29.16.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"lodash": "^4.18.1",
|
||||
"marked": ">=3 <19",
|
||||
"marked-alignment-paragraphs": "^1.0.0",
|
||||
"marked-definition-lists": "^1.0.1",
|
||||
"marked-diagrams-markdeep": "^1.0.1",
|
||||
"marked-emoji": "^2.0.3",
|
||||
"marked-extended-tables": "^2.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.4",
|
||||
"marked-nonbreaking-spaces": "^1.0.1",
|
||||
"marked-smartypants-lite": "^1.0.3",
|
||||
"marked-subsuper-text": "^1.0.4",
|
||||
"marked-variables": "^1.0.7"
|
||||
"marked-alignment-paragraphs": "1.0.0",
|
||||
"marked-definition-lists": "1.0.1",
|
||||
"marked-diagrams-markdeep": "1.0.1",
|
||||
"marked-emoji": "3.0.0",
|
||||
"marked-extended-tables": "2.0.1",
|
||||
"marked-gfm-heading-id": "4.1.4",
|
||||
"marked-nonbreaking-spaces": "1.0.1",
|
||||
"marked-smartypants-lite": "1.0.3",
|
||||
"marked-subsuper-text": "1.0.4",
|
||||
"marked-variables": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked-nonbreaking-spaces": {
|
||||
@@ -11617,9 +11617,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked-variables": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/marked-variables/-/marked-variables-1.0.7.tgz",
|
||||
"integrity": "sha512-b7ZxKQJ9TEF/xi7dpP//IyIvK10minaYqmQNX7K91K9189jSQ8grpxY9yOIAXpPmVfReY0aZbfxugolTBIpXAA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked-variables/-/marked-variables-2.0.0.tgz",
|
||||
"integrity": "sha512-U3LQ7mXfXpM0H2jExenENOvuFrP4JzSksRUr0yyENxZEkK3jmSlUEdT9LBuYxjOqWlMMhqiFVPOmtm1/QJnS+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expr-eval": "^2.0.2",
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.24.1",
|
||||
"version": "3.24.2",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"npm": ">=10.8 <12",
|
||||
@@ -128,7 +128,7 @@
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^4.9.1",
|
||||
"lodash": "^4.18.1",
|
||||
"marked-hbfm": "^1.0.1",
|
||||
"marked-hbfm": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.31.0",
|
||||
"mongoose": "^9.11.2",
|
||||
|
||||
+78
-135
@@ -2,7 +2,6 @@
|
||||
import _ from 'lodash';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
import express from 'express';
|
||||
import zlib from 'zlib';
|
||||
import GoogleActions from './googleActions.js';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import * as yaml from 'js-yaml';
|
||||
@@ -27,17 +26,8 @@ const isStaticTheme = (renderer, themeName)=>{
|
||||
return Themes[renderer]?.[themeName] !== undefined;
|
||||
};
|
||||
|
||||
// const getTopBrews = (cb) => {
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
// });
|
||||
// };
|
||||
|
||||
|
||||
const migrateSystemsToTags = (brew)=>{
|
||||
if(!('systems' in brew)) return brew;
|
||||
|
||||
if(!Array.isArray(brew.systems) || brew.systems.length === 0) {
|
||||
if(!('systems' in brew) || !Array.isArray(brew.systems) || brew.systems.length === 0) {
|
||||
brew.systems = undefined;
|
||||
return brew;
|
||||
}
|
||||
@@ -108,19 +98,17 @@ const api = {
|
||||
|
||||
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
|
||||
|
||||
if(brews) {
|
||||
for (const brew of brews) {
|
||||
userThemes[brew.renderer] ??= {};
|
||||
userThemes[brew.renderer][brew.shareId] = {
|
||||
name : brew.title,
|
||||
renderer : brew.renderer,
|
||||
baseTheme : brew.theme,
|
||||
baseSnippets : false,
|
||||
author : brew.authors[0],
|
||||
path : brew.shareId,
|
||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||
};
|
||||
}
|
||||
for (const brew of brews) {
|
||||
userThemes[brew.renderer] ??= {};
|
||||
userThemes[brew.renderer][brew.shareId] = {
|
||||
name : brew.title,
|
||||
renderer : brew.renderer,
|
||||
baseTheme : brew.theme,
|
||||
baseSnippets : false,
|
||||
author : brew.authors[0],
|
||||
path : brew.shareId,
|
||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||
};
|
||||
}
|
||||
|
||||
return userThemes;
|
||||
@@ -189,18 +177,14 @@ const api = {
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
|
||||
|
||||
|
||||
const fixedStub = migrateSystemsToTags(stub);
|
||||
req.brew = fixedStub;
|
||||
req.brew = migrateSystemsToTags(stub);
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
next();
|
||||
};
|
||||
},
|
||||
getCSS : async (req, res)=>{
|
||||
const { brew } = req;
|
||||
if(!brew) return res.status(404).send('');
|
||||
if(!brew.style) return res.status(404).send('');
|
||||
if(!brew?.style) return res.status(404).send('');
|
||||
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
@@ -216,19 +200,15 @@ const api = {
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
}
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme']);
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme', 'columns', 'columnGutter', 'license', 'legalAuthors']);
|
||||
const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets;
|
||||
metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined;
|
||||
metadata.bleedSize = { top: brew?.bleedSize?.top, bottom: brew?.bleedSize?.bottom, inner: brew?.bleedSize?.inner, outer: brew?.bleedSize?.outer };
|
||||
metadata.safetySpace = { top: brew?.safetySpace?.top, bottom: brew?.safetySpace?.bottom, outer: brew?.safetySpace?.outer, inner: brew?.safetySpace?.inner };
|
||||
metadata.trimSize = { width: brew?.trimSize?.width, height: brew?.trimSize?.height };
|
||||
metadata.columns = brew?.columns;
|
||||
metadata.columnGutter = brew?.columnGutter;
|
||||
metadata.license = brew?.license;
|
||||
metadata.legalAuthors = brew?.legalAuthors;
|
||||
|
||||
text = `\`\`\`metadata\n` +
|
||||
`${yaml.dump(metadata)}\n` +
|
||||
`${yaml.dump(metadata)}` +
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
return text;
|
||||
@@ -238,28 +218,14 @@ const api = {
|
||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||
.slice(0, MAX_TITLE_LENGTH);
|
||||
},
|
||||
excludePropsFromUpdate : (brew)=>{
|
||||
// Remove undesired properties
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['_id', 'views', 'lastViewed'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
excludePropsFromUpdate : (brew)=>{ //Remove props that should not update/overwrite when editing
|
||||
return _.omit(brew, ['_id', 'views', 'lastViewed']);
|
||||
},
|
||||
excludeGoogleProps : (brew)=>{
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
excludeGoogleProps : (brew)=>{ //Remove props that should not be saved to Google (only stored to Homebrewery stub)
|
||||
return _.omit(brew, ['version', 'tags', 'systems', 'published', 'authors', 'invitedAuthors', 'owner', 'views', 'thumbnail']);
|
||||
},
|
||||
excludeStubProps : (brew)=>{
|
||||
const propsToExclude = ['text', 'textBin'];
|
||||
for (const prop of propsToExclude) {
|
||||
brew[prop] = undefined;
|
||||
}
|
||||
excludeStubProps : (brew)=>{ //Remove props that should not be saved to stub-only brews
|
||||
brew.text = brew.textBin = undefined; // Can't use _.omit because this brew is a Mongoose object
|
||||
return brew;
|
||||
},
|
||||
beforeNewSave : (account, brew)=>{
|
||||
@@ -278,9 +244,7 @@ const api = {
|
||||
newGoogleBrew : async (account, brew, res)=>{
|
||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||
|
||||
const newBrew = api.excludeGoogleProps(brew);
|
||||
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, api.excludeGoogleProps(brew));
|
||||
},
|
||||
newBrew : async (req, res)=>{
|
||||
const brew = req.body;
|
||||
@@ -292,7 +256,7 @@ const api = {
|
||||
|
||||
api.beforeNewSave(req.account, brew);
|
||||
|
||||
const newHomebrew = new HomebrewModel(brew);
|
||||
let newHomebrew = new HomebrewModel(brew);
|
||||
newHomebrew.editId = nanoid(12);
|
||||
newHomebrew.shareId = nanoid(12);
|
||||
|
||||
@@ -301,15 +265,10 @@ const api = {
|
||||
googleId = await api.newGoogleBrew(req.account, newHomebrew, res);
|
||||
|
||||
if(!googleId) return;
|
||||
api.excludeStubProps(newHomebrew);
|
||||
newHomebrew = api.excludeStubProps(newHomebrew);
|
||||
newHomebrew.googleId = googleId;
|
||||
} else {
|
||||
// Compress brew text to binary before saving
|
||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
newHomebrew.text = undefined;
|
||||
}
|
||||
|
||||
|
||||
saved = await newHomebrew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err, err.toString(), err.stack);
|
||||
@@ -380,7 +339,6 @@ const api = {
|
||||
return res.status(200).send(returnObj);
|
||||
},
|
||||
updateBrew : async (req, res)=>{
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
const brewFromServer = req.brew;
|
||||
|
||||
@@ -406,16 +364,17 @@ const api = {
|
||||
const patches = parsePatch(brewFromClient.patches);
|
||||
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||
result = applyPatches(patches, encodeURI(brewFromServer.text));
|
||||
const failedPatches = patches.map((patch, index)=>{if(!result[1][index]){ return patch; }});
|
||||
if(failedPatches > 0){
|
||||
throw (`Patch failure: ${failedPatches}/${result[1].length} did not apply`);
|
||||
const failedPatchCount = result[1].filter((patchApplied)=>!patchApplied).length; //Count how many patches failed to apply
|
||||
if(failedPatchCount){
|
||||
throw (`Patch failure: ${failedPatchCount}/${result[1].length} did not apply`);
|
||||
}
|
||||
if(decodeURI(result[0]) != brewFromClient.text){
|
||||
patchedText = decodeURI(result[0]);
|
||||
if(patchedText != brewFromClient.text){
|
||||
debugTextMismatch(brewFromClient.text, patchedText, `edit/${brewFromClient.editId}`);
|
||||
throw ('Patches did not apply cleanly, text mismatch detected');
|
||||
}
|
||||
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||
} catch (err) {
|
||||
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
console.error('Failed to apply patches:', {
|
||||
// patches : brewFromClient.patches,
|
||||
// result : result,
|
||||
@@ -426,77 +385,63 @@ const api = {
|
||||
// throw err; // rethrow to preserve the 500 behavior
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
|
||||
migrateSystemsToTags(brew);
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
|
||||
brew = migrateSystemsToTags(brew);
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
const googleId = brew.googleId;
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
let afterSave = async ()=>true;
|
||||
|
||||
if(brew.googleId && removeFromGoogle) {
|
||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||
afterSave = async ()=>{
|
||||
return await api.deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||
});
|
||||
};
|
||||
|
||||
brew.googleId = undefined;
|
||||
} else if(!brew.googleId && saveToGoogle) {
|
||||
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
||||
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res);
|
||||
|
||||
if(!brew.googleId) return;
|
||||
} else if(brew.googleId) {
|
||||
// If the google id exists and no other actions are being performed, update the google brew
|
||||
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew), req.ip);
|
||||
|
||||
if(!updated) return;
|
||||
}
|
||||
|
||||
if(brew.googleId) {
|
||||
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
||||
api.excludeStubProps(brew);
|
||||
} else {
|
||||
// Compress brew text to binary before saving
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
brew.text = undefined;
|
||||
}
|
||||
brew.updatedAt = new Date();
|
||||
brew.version = (brew.version || 1) + 1;
|
||||
|
||||
brew.updatedAt = new Date();
|
||||
brew.version = (brew.version || 1) + 1;
|
||||
if(req.account) {
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
|
||||
}
|
||||
|
||||
// define a function to catch our save errors
|
||||
const saveError = (err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||
};
|
||||
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);
|
||||
} 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);
|
||||
//=====----- Update to or remove from Google Drive -----=========//
|
||||
const originalGoogleId = brew.googleId;
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
|
||||
if(brew.googleId && removeFromGoogle) { // If removing from google, remove the brew's googleId
|
||||
brew.googleId = undefined;
|
||||
}
|
||||
else if(!brew.googleId && saveToGoogle) { // If adding to google, create new google document and record the googleId
|
||||
brew.googleId = await api.newGoogleBrew(req.account, brew, res);
|
||||
if(!brew.googleId) return;
|
||||
}
|
||||
else if(brew.googleId) { // If already on google, update the google brew
|
||||
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew), req.ip);
|
||||
if(!updated) return;
|
||||
}
|
||||
|
||||
//=====----- Update to Mongo -----=====//
|
||||
if(brew.googleId) {
|
||||
// If google id exists after above actions, Mongo will store only a stub; exclude non-stub properties from update
|
||||
brew = api.excludeStubProps(brew);
|
||||
}
|
||||
|
||||
if(brew._id) // if the brew has a stub id, fetch and merge into Mongo stub document
|
||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||
else // if not, create a new Mongo document
|
||||
brew = new HomebrewModel(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;
|
||||
// Call and wait for afterSave to complete
|
||||
const after = await afterSave();
|
||||
if(!after) return;
|
||||
|
||||
if(originalGoogleId && removeFromGoogle) {
|
||||
const deleted = await api.deleteGoogleBrew(req.account, originalGoogleId, brew.editId, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||
});
|
||||
|
||||
if(!deleted) return;
|
||||
}
|
||||
|
||||
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
|
||||
|
||||
@@ -549,8 +494,6 @@ const api = {
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
||||
brew.googleId = undefined;
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
brew.text = undefined;
|
||||
}
|
||||
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
||||
await brew.save()
|
||||
|
||||
+70
-63
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
import dedent from 'dedent';
|
||||
|
||||
describe('Tests for api', ()=>{
|
||||
let api;
|
||||
@@ -493,35 +494,35 @@ describe('Tests for api', ()=>{
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(`\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
renderer: v3
|
||||
theme: phb
|
||||
bleedSize:
|
||||
top: .125in
|
||||
bottom: .125in
|
||||
inner: .125in
|
||||
outer: .125in
|
||||
safetySpace:
|
||||
top: .25in
|
||||
bottom: .25in
|
||||
outer: .25in
|
||||
inner: .5in
|
||||
trimSize:
|
||||
width: 8.5in
|
||||
height: 11in
|
||||
columns: '2'
|
||||
columnGutter: .125in
|
||||
license: None
|
||||
legalAuthors: ''
|
||||
expect(result).toEqual(dedent`
|
||||
\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
renderer: v3
|
||||
theme: phb
|
||||
columns: '2'
|
||||
columnGutter: .125in
|
||||
license: None
|
||||
legalAuthors: ''
|
||||
bleedSize:
|
||||
top: .125in
|
||||
bottom: .125in
|
||||
inner: .125in
|
||||
outer: .125in
|
||||
safetySpace:
|
||||
top: .25in
|
||||
bottom: .25in
|
||||
outer: .25in
|
||||
inner: .5in
|
||||
trimSize:
|
||||
width: 8.5in
|
||||
height: 11in
|
||||
\`\`\`
|
||||
|
||||
\`\`\`
|
||||
|
||||
brew`);
|
||||
brew`);
|
||||
});
|
||||
|
||||
it('should set metadata and style', ()=>{
|
||||
@@ -556,39 +557,39 @@ brew`);
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(`\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
renderer: v3
|
||||
theme: phb
|
||||
bleedSize:
|
||||
top: .125in
|
||||
bottom: .125in
|
||||
inner: .125in
|
||||
outer: .125in
|
||||
safetySpace:
|
||||
top: .25in
|
||||
bottom: .25in
|
||||
outer: .25in
|
||||
inner: .5in
|
||||
trimSize:
|
||||
width: 8.5in
|
||||
height: 11in
|
||||
columns: '2'
|
||||
columnGutter: .125in
|
||||
license: None
|
||||
legalAuthors: ''
|
||||
expect(result).toEqual(dedent`
|
||||
\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
renderer: v3
|
||||
theme: phb
|
||||
columns: '2'
|
||||
columnGutter: .125in
|
||||
license: None
|
||||
legalAuthors: ''
|
||||
bleedSize:
|
||||
top: .125in
|
||||
bottom: .125in
|
||||
inner: .125in
|
||||
outer: .125in
|
||||
safetySpace:
|
||||
top: .25in
|
||||
bottom: .25in
|
||||
outer: .25in
|
||||
inner: .5in
|
||||
trimSize:
|
||||
width: 8.5in
|
||||
height: 11in
|
||||
\`\`\`
|
||||
|
||||
\`\`\`
|
||||
\`\`\`css
|
||||
hello yes i am css
|
||||
\`\`\`
|
||||
|
||||
\`\`\`css
|
||||
hello yes i am css
|
||||
\`\`\`
|
||||
|
||||
brew`);
|
||||
brew`);
|
||||
});
|
||||
});
|
||||
describe('exclusion methods', ()=>{
|
||||
@@ -700,8 +701,15 @@ brew`);
|
||||
shareId : expect.any(String),
|
||||
style : undefined,
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : expect.objectContaining({}),
|
||||
text : '```metadata\n' +
|
||||
'title: asdf\n' +
|
||||
'bleedSize: {}\n' +
|
||||
'safetySpace: {}\n' +
|
||||
'trimSize: {}' +
|
||||
'\n' +
|
||||
'```\n' +
|
||||
'\n' +
|
||||
'asdf',
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
title : 'asdf',
|
||||
@@ -1233,8 +1241,7 @@ brew`);
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test2']);
|
||||
expect(saved.googleId).toEqual(undefined);
|
||||
expect(saved.text).toEqual(undefined);
|
||||
expect(saved.textBin).not.toEqual(undefined);
|
||||
expect(saved.text).not.toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should retain google brew and update stub when multiple authors and extra author requests deletion', async ()=>{
|
||||
|
||||
@@ -32,14 +32,21 @@ const HomebrewSchema = mongoose.Schema({
|
||||
lock : { type: Object, index: true }
|
||||
}, { versionKey: false });
|
||||
|
||||
HomebrewSchema.pre('save', function() {
|
||||
if(this.text !== undefined) {
|
||||
this.textBin = zlib.deflateRawSync(this.text);
|
||||
this.text = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
HomebrewSchema.statics.increaseView = async function(query) {
|
||||
const brew = await Homebrew.findOne(query).exec();
|
||||
brew.lastViewed = new Date();
|
||||
brew.views = brew.views + 1;
|
||||
await brew.save()
|
||||
.catch((err)=>{
|
||||
return err;
|
||||
});
|
||||
.catch((err)=>{
|
||||
return err;
|
||||
});
|
||||
return brew;
|
||||
};
|
||||
|
||||
@@ -51,6 +58,7 @@ HomebrewSchema.statics.get = async function(query, fields=null){
|
||||
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
|
||||
const unzipped = zlib.inflateRawSync(brew.textBin);
|
||||
brew.text = unzipped.toString();
|
||||
brew.textBin = undefined;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user