diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3c48e7d34..3049a872a 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -48,7 +48,7 @@ jobs:
- image: cimg/node:16.11.0
working_directory: ~/homebrewery
- parallelism: 4
+ parallelism: 1
steps:
- attach_workspace:
@@ -61,15 +61,15 @@ jobs:
- run:
name: Test - Basic
command: npm run test:basic
- - run:
- name: Test - Coverage
- command: npm run test:coverage
- run:
name: Test - Mustache Spans
command: npm run test:mustache-span
- run:
name: Test - Routes
command: npm run test:route
+ - run:
+ name: Test - Coverage
+ command: npm run test:coverage
workflows:
build_and_test:
diff --git a/changelog.md b/changelog.md
index 10899a485..60719b916 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,23 @@
```css
+.beta {
+ color : white;
+ padding : 4px 6px;
+ line-height : 1em;
+ background : grey;
+ border-radius : 12px;
+ font-family : monospace;
+ font-size : 10px;
+ font-weight : 800;
+ margin-top : -5px;
+ margin-bottom : -5px;
+}
+
+.fac {
+ height: 1em;
+ line-height: 2em;
+ margin-bottom: -0.05cm
+}
+
h5 {
font-size: .35cm !important;
}
@@ -61,6 +80,76 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
+### XXXXday DD/MM/2023 - v3.8.0
+{{taskList
+##### G-Ambatte
+
+* [x] Update server build scripts to fix Admin page
+
+Fixes issues [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
+
+* [x] Fix internal links inside `
` blocks not automatically receiving the `target=_self` attribute
+
+Fixes issues [#2680](https://github.com/naturalcrit/homebrewery/issues/2680)
+}}
+
+### Monday 13/03/2023 - v3.7.2
+{{taskList
+
+##### Calculuschild
+
+* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
+}}
+
+### Thursday 09/03/2023 - v3.7.1
+{{taskList
+
+##### Lucastucious (new contributor!)
+
+* [x] Changed `filter: drop-shadow` to `box-shadow` on text boxes, making text selectable in PDFs again.
+
+Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
+
+{{note
+**NOTE:** If you create your PDF on a computer with an old version of Mac Preview (v10 or older) you may see shadows appear as solid gray.
+}}
+
+##### MichielDeMey
+
+* [x] Updated the Google Drive icon
+* [x] Backend fix to unit tests failing intermittently
+
+##### Calculuschild
+
+* [x] Fix PDF pixelation on CoverPage text outlines
+}}
+
+
+### Tuesday 28/02/2023 - v3.7.0
+{{taskList
+
+{{note
+**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
+}}
+
+##### Calculuschild
+
+* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
+
+* [x] Fix site not displaying on iOS devices
+
+##### 5e-Cleric
+
+* [x] New {{openSans **PHB → COVER PAGE** {{fac,book-front-cover}} }} snippet for V3, which adds a stylish coverpage to your brew! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
+
+##### MichielDeMey (new contribuor!)
+
+* [x] Fix typo in testing scripts
+* [x] Fix "mug" image not using HTTPS
+
+Fixes issues [#2687](https://github.com/naturalcrit/homebrewery/issues/2687)
+}}
+
### Saturday 18/02/2023 - v3.6.1
{{taskList
##### G-Ambatte
@@ -70,8 +159,7 @@ For a full record of development, visit our [Github Page](https://github.com/nat
Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674)
}}
-
-### Friday 23/01/2023 - v3.6.0
+### Monday 23/01/2023 - v3.6.0
{{taskList
##### calculuschild
diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx
new file mode 100644
index 000000000..a6e699dcf
--- /dev/null
+++ b/client/components/combobox.jsx
@@ -0,0 +1,129 @@
+const React = require('react');
+const createClass = require('create-react-class');
+const _ = require('lodash');
+const cx = require('classnames');
+require('./combobox.less');
+
+const Combobox = createClass({
+ displayName : 'Combobox',
+ getDefaultProps : function() {
+ return {
+ className : '',
+ trigger : 'hover',
+ default : '',
+ placeholder : '',
+ autoSuggest : {
+ clearAutoSuggestOnClick : true,
+ suggestMethod : 'includes',
+ filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
+ },
+ };
+ },
+ getInitialState : function() {
+ return {
+ showDropdown : false,
+ value : '',
+ options : [...this.props.options],
+ inputFocused : false
+ };
+ },
+ componentDidMount : function() {
+ if(this.props.trigger == 'click')
+ document.addEventListener('click', this.handleClickOutside);
+ this.setState({
+ value : this.props.default
+ });
+ },
+ componentWillUnmount : function() {
+ if(this.props.trigger == 'click')
+ document.removeEventListener('click', this.handleClickOutside);
+ },
+ handleClickOutside : function(e){
+ // Close dropdown when clicked outside
+ if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
+ this.handleDropdown(false);
+ }
+ },
+ handleDropdown : function(show){
+ this.setState({
+ showDropdown : show,
+ inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
+ });
+ },
+ handleInput : function(e){
+ e.persist();
+ this.setState({
+ value : e.target.value,
+ inputFocused : false
+ }, ()=>{
+ this.props.onEntry(e);
+ });
+ },
+ handleSelect : function(e){
+ this.setState({
+ value : e.currentTarget.getAttribute('data-value')
+ }, ()=>{this.props.onSelect(this.state.value);});
+ ;
+ },
+ renderTextInput : function(){
+ return (
+
{this.handleDropdown(true);} : undefined}
+ onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}>
+ this.handleInput(e)}
+ value={this.state.value || ''}
+ placeholder={this.props.placeholder}
+ onBlur={(e)=>{
+ if(!e.target.checkValidity()){
+ this.setState({
+ value : this.props.default
+ }, ()=>this.props.onEntry(e));
+ }
+ }}
+ />
+
+ );
+ },
+ renderDropdown : function(dropdownChildren){
+ if(!this.state.showDropdown) return null;
+ if(this.props.autoSuggest && !this.state.inputFocused){
+ const suggestMethod = this.props.autoSuggest.suggestMethod;
+ const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
+ const filteredArrays = filterOn.map((attr)=>{
+ const children = dropdownChildren.filter((item)=>{
+ if(suggestMethod === 'includes'){
+ return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
+ } else if(suggestMethod === 'startsWith'){
+ return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
+ }
+ });
+ return children;
+ });
+ dropdownChildren = _.uniq(filteredArrays.flat(1));
+ }
+
+ return (
+
+ {dropdownChildren}
+
+ );
+ },
+ render : function () {
+ const dropdownChildren = this.state.options.map((child, i)=>{
+ const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) });
+ return clone;
+ });
+ return (
+
{this.handleDropdown(false);} : undefined}>
+ {this.renderTextInput()}
+ {this.renderDropdown(dropdownChildren)}
+
+ );
+ }
+});
+
+module.exports = Combobox;
diff --git a/client/components/combobox.less b/client/components/combobox.less
new file mode 100644
index 000000000..3810a874e
--- /dev/null
+++ b/client/components/combobox.less
@@ -0,0 +1,50 @@
+.dropdown-container {
+ position:relative;
+ input {
+ width: 100%;
+ }
+ .dropdown-options {
+ position:absolute;
+ background-color: white;
+ z-index: 100;
+ width: 100%;
+ border: 1px solid gray;
+ overflow-y: auto;
+ max-height: 200px;
+
+ &::-webkit-scrollbar {
+ width: 14px;
+ }
+ &::-webkit-scrollbar-track {
+ background: #ffffff;
+ }
+ &::-webkit-scrollbar-thumb {
+ background-color: #949494;
+ border-radius: 10px;
+ border: 3px solid #ffffff;
+ }
+
+ .item {
+ position:relative;
+ font-size: 11px;
+ font-family: Open Sans;
+ padding: 5px;
+ cursor: default;
+ margin: 0 3px;
+ //border-bottom: 1px solid darkgray;
+ &:hover {
+ filter: brightness(120%);
+ background-color: rgb(163, 163, 163);
+ }
+ .detail {
+ width:100%;
+ text-align: left;
+ color: rgb(124, 124, 124);
+ font-style:italic;
+ font-size: 9px;
+ }
+ }
+
+ }
+
+}
diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx
index a41e01228..27fef7e16 100644
--- a/client/homebrew/brewRenderer/brewRenderer.jsx
+++ b/client/homebrew/brewRenderer/brewRenderer.jsx
@@ -27,6 +27,7 @@ const BrewRenderer = createClass({
style : '',
renderer : 'legacy',
theme : '5ePHB',
+ lang : '',
errors : []
};
},
@@ -190,7 +191,6 @@ const BrewRenderer = createClass({
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = this.props.theme ?? '5ePHB';
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
-
return (
{!this.state.isMounted
@@ -223,7 +223,7 @@ const BrewRenderer = createClass({
&&
<>
{this.renderStyle()}
-
+
{this.renderPages()}
>
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
index 3d77f557f..074879b05 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
@@ -6,6 +6,7 @@ const _ = require('lodash');
const cx = require('classnames');
const request = require('../../utils/request-middleware.js');
const Nav = require('naturalcrit/nav/nav.jsx');
+const Combobox = require('client/components/combobox.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
const Themes = require('themes/themes.json');
@@ -35,7 +36,8 @@ const MetadataEditor = createClass({
authors : [],
systems : [],
renderer : 'legacy',
- theme : '5ePHB'
+ theme : '5ePHB',
+ lang : 'en'
},
onChange : ()=>{},
reportError : ()=>{}
@@ -76,6 +78,7 @@ const MetadataEditor = createClass({
const errMessage = validationErr.map((err)=>{
return `- ${err}`;
}).join('\n');
+
callIfExists(e.target, 'setCustomValidity', errMessage);
callIfExists(e.target, 'reportValidity');
}
@@ -111,6 +114,11 @@ const MetadataEditor = createClass({
this.props.onChange(this.props.metadata);
},
+ 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){
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;
@@ -224,6 +232,47 @@ const MetadataEditor = createClass({
;
},
+ renderLanguageDropdown : function(){
+ const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant'];
+ const listLanguages = ()=>{
+ return _.map(langCodes.sort(), (code, index)=>{
+ const localName = new Intl.DisplayNames([code], { type: 'language' });
+ const englishName = new Intl.DisplayNames('en', { type: 'language' });
+ return
+ {`${code}`}
+
{`${localName.of(code)}`}
+
;
+ });
+ };
+
+ const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
+
+ return
+
language
+
+ this.handleLanguage(value)}
+ onEntry={(e)=>{
+ e.target.setCustomValidity(''); //Clear the validation popup while typing
+ debouncedHandleFieldChange('lang', e);
+ }}
+ options={listLanguages()}
+ autoSuggest={{
+ suggestMethod : 'startsWith',
+ clearAutoSuggestOnClick : true,
+ filterOn : ['data-value', 'data-detail', 'title']
+ }}
+ >
+
+ Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.
+
+
+
;
+ },
+
renderRenderOptions : function(){
if(!global.enable_v3) return;
@@ -301,6 +350,8 @@ const MetadataEditor = createClass({
+ {this.renderLanguageDropdown()}
+
{this.renderThemeDropdown()}
{this.renderRenderOptions()}
@@ -315,7 +366,7 @@ const MetadataEditor = createClass({
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.']}
+ notes={['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.']}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
this.handleSnippetClick(e, snippet)}>
- {snippet.name}
+
{snippet.name}
+ {snippet.experimental &&
beta }
{snippet.subsnippets && <>
diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less
index 9c6bf3f08..cb24da105 100644
--- a/client/homebrew/editor/snippetbar/snippetbar.less
+++ b/client/homebrew/editor/snippetbar/snippetbar.less
@@ -96,20 +96,36 @@
padding : 0px;
background-color : #ddd;
.snippet{
+ position: relative;
.animate(background-color);
- min-width : max-content;
- padding : 5px;
- cursor : pointer;
- font-size : 10px;
+ display : flex;
+ align-items : center;
+ min-width : max-content;
+ padding : 5px;
+ cursor : pointer;
+ font-size : 10px;
i{
margin-right : 8px;
font-size : 1.2em;
height : 1.2em;
&~i{
margin-right: 0;
- margin-left: 8px;
+ margin-left: 5px;
}
}
+ .name {
+ margin-right : auto;
+ }
+ .beta {
+ color : white;
+ padding : 4px 6px;
+ line-height : 1em;
+ margin-left : 5px;
+ align-self : center;
+ background : grey;
+ border-radius : 12px;
+ font-family : monospace;
+ }
&:hover{
background-color : #999;
&>.dropdown{
diff --git a/client/homebrew/googleDrive.png b/client/homebrew/googleDrive.png
deleted file mode 100644
index f555103cc..000000000
Binary files a/client/homebrew/googleDrive.png and /dev/null differ
diff --git a/client/homebrew/googleDrive.svg b/client/homebrew/googleDrive.svg
new file mode 100644
index 000000000..a8cefd5b2
--- /dev/null
+++ b/client/homebrew/googleDrive.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/homebrew/googleDriveMono.png b/client/homebrew/googleDriveMono.png
deleted file mode 100644
index a573196d7..000000000
Binary files a/client/homebrew/googleDriveMono.png and /dev/null differ
diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx
index 5fa225945..f6dccbdb7 100644
--- a/client/homebrew/homebrew.jsx
+++ b/client/homebrew/homebrew.jsx
@@ -47,6 +47,7 @@ const Homebrew = createClass({
editId : null,
createdAt : null,
updatedAt : null,
+ lang : ''
}
};
},
diff --git a/client/homebrew/navbar/account.navitem.jsx b/client/homebrew/navbar/account.navitem.jsx
index 7347e084b..6b412c368 100644
--- a/client/homebrew/navbar/account.navitem.jsx
+++ b/client/homebrew/navbar/account.navitem.jsx
@@ -63,7 +63,7 @@ const Account = createClass({
if(global.account){
return
diff --git a/client/homebrew/navbar/navbar.less b/client/homebrew/navbar/navbar.less
index 3bd6a66e1..b1db6ae30 100644
--- a/client/homebrew/navbar/navbar.less
+++ b/client/homebrew/navbar/navbar.less
@@ -187,4 +187,7 @@
.account.navItem{
min-width: 100px;
}
+ .account.username.navItem{
+ text-transform: none;
+ }
}
diff --git a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx
index aa2f47431..99bcc9351 100644
--- a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx
+++ b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx
@@ -6,7 +6,7 @@ const cx = require('classnames');
const moment = require('moment');
const request = require('../../../../utils/request-middleware.js');
-const googleDriveIcon = require('../../../../googleDrive.png');
+const googleDriveIcon = require('../../../../googleDrive.svg');
const dedent = require('dedent-tabs').default;
const BrewItem = createClass({
diff --git a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less
index 42e0fd07e..da46eeb01 100644
--- a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less
+++ b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less
@@ -94,7 +94,7 @@
}
}
.googleDriveIcon {
- height : 20px;
+ height : 18px;
padding : 0px;
margin : -5px;
}
diff --git a/client/homebrew/pages/basePages/listPage/listPage.jsx b/client/homebrew/pages/basePages/listPage/listPage.jsx
index 45fb30ac4..86570ec46 100644
--- a/client/homebrew/pages/basePages/listPage/listPage.jsx
+++ b/client/homebrew/pages/basePages/listPage/listPage.jsx
@@ -219,7 +219,7 @@ const ListPage = createClass({
render : function(){
return
-
+ {/**/}
{this.props.navItems}
{this.renderSortOptions()}
diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx
index 87bc7eefb..94d5aef3b 100644
--- a/client/homebrew/pages/editPage/editPage.jsx
+++ b/client/homebrew/pages/editPage/editPage.jsx
@@ -24,8 +24,7 @@ const Markdown = require('naturalcrit/markdown.js');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
-const googleDriveActive = require('../../googleDrive.png');
-const googleDriveInactive = require('../../googleDriveMono.png');
+const googleDriveIcon = require('../../googleDrive.svg');
const SAVE_TIMEOUT = 3000;
@@ -222,10 +221,7 @@ const EditPage = createClass({
renderGoogleDriveIcon : function(){
return
- {this.state.saveGoogle
- ?
- :
- }
+
{this.state.confirmGoogleTransfer &&
@@ -402,7 +398,14 @@ const EditPage = createClass({
reportError={this.errorReported}
renderer={this.state.brew.renderer}
/>
-
+
;
diff --git a/client/homebrew/pages/editPage/editPage.less b/client/homebrew/pages/editPage/editPage.less
index 581e6dfa2..f94b60c1b 100644
--- a/client/homebrew/pages/editPage/editPage.less
+++ b/client/homebrew/pages/editPage/editPage.less
@@ -18,8 +18,12 @@
position : relative;
}
.googleDriveStorage img{
- height : 20px;
+ height : 18px;
padding : 0px;
margin : -5px;
+
+ &.inactive {
+ filter: grayscale(1);
+ }
}
}
diff --git a/client/homebrew/pages/homePage/welcome_msg.md b/client/homebrew/pages/homePage/welcome_msg.md
index deee966ea..3332dfc39 100644
--- a/client/homebrew/pages/homePage/welcome_msg.md
+++ b/client/homebrew/pages/homePage/welcome_msg.md
@@ -36,7 +36,7 @@ After clicking the "Print" item in the navbar a new page will open and a print d
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
}}
- {position:absolute,bottom:20px,left:130px,width:220px}
+ {position:absolute,bottom:20px,left:130px,width:220px}
{{artist,bottom:160px,left:100px
##### Homebrew Mug
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx
index 3e96ff5c0..0f18d42be 100644
--- a/client/homebrew/pages/newPage/newPage.jsx
+++ b/client/homebrew/pages/newPage/newPage.jsx
@@ -61,6 +61,7 @@ const NewPage = createClass({
// brew.description = metaStorage?.description || this.state.brew.description;
brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme;
+ brew.lang = metaStorage?.lang ?? brew.lang;
this.setState({
brew : brew
@@ -70,7 +71,7 @@ const NewPage = createClass({
localStorage.setItem(BREWKEY, brew.text);
if(brew.style)
localStorage.setItem(STYLEKEY, brew.style);
- localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme }));
+ localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
},
componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys);
@@ -114,13 +115,16 @@ const NewPage = createClass({
handleMetaChange : function(metadata){
this.setState((prevState)=>({
brew : { ...prevState.brew, ...metadata },
- }));
- localStorage.setItem(METAKEY, JSON.stringify({
- // 'title' : this.state.brew.title,
- // 'description' : this.state.brew.description,
- 'renderer' : this.state.brew.renderer,
- 'theme' : this.state.brew.theme
- }));
+ }), ()=>{
+ localStorage.setItem(METAKEY, JSON.stringify({
+ // 'title' : this.state.brew.title,
+ // 'description' : this.state.brew.description,
+ 'renderer' : this.state.brew.renderer,
+ 'theme' : this.state.brew.theme,
+ 'lang' : this.state.brew.lang
+ }));
+ });
+ ;
},
save : async function(){
@@ -211,7 +215,7 @@ const NewPage = createClass({
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer}
/>
-
+
;
diff --git a/client/icons/Davek.svg b/client/icons/Davek.svg
new file mode 100644
index 000000000..0f2324ce2
--- /dev/null
+++ b/client/icons/Davek.svg
@@ -0,0 +1 @@
+