mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-08 20:23:39 +00:00
Merge pull request #3923 from dbolack-ab/writeinBrewTheme
WIP: User Brew Theme Write-in
This commit is contained in:
@@ -45,6 +45,7 @@ const Combobox = createClass({
|
|||||||
},
|
},
|
||||||
handleDropdown : function(show){
|
handleDropdown : function(show){
|
||||||
this.setState({
|
this.setState({
|
||||||
|
value : show ? '' : this.props.default,
|
||||||
showDropdown : show,
|
showDropdown : show,
|
||||||
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
||||||
});
|
});
|
||||||
@@ -78,7 +79,7 @@ const Combobox = createClass({
|
|||||||
if(!e.target.checkValidity()){
|
if(!e.target.checkValidity()){
|
||||||
this.setState({
|
this.setState({
|
||||||
value : this.props.default
|
value : this.props.default
|
||||||
}, ()=>this.props.onEntry(e));
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -456,6 +456,7 @@ const Editor = createClass({
|
|||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
|
themeBundle={this.props.themeBundle}
|
||||||
onChange={this.props.onMetaChange}
|
onChange={this.props.onMetaChange}
|
||||||
reportError={this.props.reportError}
|
reportError={this.props.reportError}
|
||||||
userThemes={this.props.userThemes}/>
|
userThemes={this.props.userThemes}/>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const MetadataEditor = createClass({
|
|||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : 'en'
|
lang : 'en'
|
||||||
},
|
},
|
||||||
|
|
||||||
onChange : ()=>{},
|
onChange : ()=>{},
|
||||||
reportError : ()=>{}
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
@@ -47,7 +48,7 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
getInitialState : function(){
|
getInitialState : function(){
|
||||||
return {
|
return {
|
||||||
showThumbnail : true
|
showThumbnail : true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -67,6 +68,11 @@ const MetadataEditor = createClass({
|
|||||||
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);
|
||||||
|
|
||||||
|
const debouncedReportValidity = _.debounce((target, errMessage) => {
|
||||||
|
callIfExists(target, 'setCustomValidity', errMessage);
|
||||||
|
callIfExists(target, 'reportValidity');
|
||||||
|
}, 300); // 300ms debounce delay, adjust as needed
|
||||||
|
|
||||||
// if no validation rules, save to props
|
// if no validation rules, save to props
|
||||||
if(validationErr.length === 0){
|
if(validationErr.length === 0){
|
||||||
callIfExists(e.target, 'setCustomValidity', '');
|
callIfExists(e.target, 'setCustomValidity', '');
|
||||||
@@ -74,14 +80,16 @@ const MetadataEditor = createClass({
|
|||||||
...this.props.metadata,
|
...this.props.metadata,
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
} 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.
|
||||||
const errMessage = validationErr.map((err)=>{
|
const errMessage = validationErr.map((err)=>{
|
||||||
return `- ${err}`;
|
return `- ${err}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
callIfExists(e.target, 'setCustomValidity', errMessage);
|
|
||||||
callIfExists(e.target, 'reportValidity');
|
debouncedReportValidity(e.target, errMessage);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -112,6 +120,14 @@ const MetadataEditor = createClass({
|
|||||||
handleTheme : function(theme){
|
handleTheme : function(theme){
|
||||||
this.props.metadata.renderer = theme.renderer;
|
this.props.metadata.renderer = theme.renderer;
|
||||||
this.props.metadata.theme = theme.path;
|
this.props.metadata.theme = theme.path;
|
||||||
|
|
||||||
|
this.props.onChange(this.props.metadata, 'theme');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleThemeWritein : function(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');
|
this.props.onChange(this.props.metadata, 'theme');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -200,7 +216,7 @@ const MetadataEditor = createClass({
|
|||||||
if(theme.path == this.props.metadata.shareId) return;
|
if(theme.path == this.props.metadata.shareId) return;
|
||||||
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||||
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||||
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
|
return <div className='item' key={`${renderer}_${theme.name}`} value={`${theme.author ?? renderer} : ${theme.name}`} data={theme} title={''}>
|
||||||
{theme.author ?? renderer} : {theme.name}
|
{theme.author ?? renderer} : {theme.name}
|
||||||
<div className='texture-container'>
|
<div className='texture-container'>
|
||||||
<img src={texture}/>
|
<img src={texture}/>
|
||||||
@@ -210,26 +226,40 @@ const MetadataEditor = createClass({
|
|||||||
<img src={preview}/>
|
<img src={preview}/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
}).filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentRenderer = this.props.metadata.renderer;
|
const currentRenderer = this.props.metadata.renderer;
|
||||||
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
|
const currentThemeDisplay = this.props.themeBundle?.name ? `${this.props.themeBundle.author ?? currentRenderer} : ${this.props.themeBundle.name}` : 'No Theme Selected';
|
||||||
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
|
|
||||||
let dropdown;
|
let dropdown;
|
||||||
|
|
||||||
if(currentRenderer == 'legacy') {
|
if(currentRenderer == 'legacy') {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='disabled value' trigger='disabled'>
|
<div className='disabled value' trigger='disabled'>
|
||||||
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
|
<div> Themes are not supported in the Legacy Renderer </div>
|
||||||
</Nav.dropdown>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='value' trigger='click'>
|
<div className='value'>
|
||||||
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
|
<Combobox trigger='click'
|
||||||
|
className='themes-dropdown'
|
||||||
{listThemes(currentRenderer)}
|
default={currentThemeDisplay}
|
||||||
</Nav.dropdown>;
|
placeholder='Select from below, or enter the Share URL or ID of a brew with the meta:theme tag'
|
||||||
|
onSelect={(value)=>this.handleTheme(value)}
|
||||||
|
onEntry={(e)=>{
|
||||||
|
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||||
|
if(this.handleFieldChange('theme', e))
|
||||||
|
this.handleThemeWritein(e);
|
||||||
|
}}
|
||||||
|
options={listThemes(currentRenderer)}
|
||||||
|
autoSuggest={{
|
||||||
|
suggestMethod : 'includes',
|
||||||
|
clearAutoSuggestOnClick : true,
|
||||||
|
filterOn : ['value', 'title']
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<small>Select from the list below (built-in themes and brews you have tagged "meta:theme"), or paste in the Share URL or Share ID of any brew.</small>
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className='field themes'>
|
return <div className='field themes'>
|
||||||
@@ -251,8 +281,6 @@ const MetadataEditor = createClass({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
|
|
||||||
|
|
||||||
return <div className='field language'>
|
return <div className='field language'>
|
||||||
<label>language</label>
|
<label>language</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -263,7 +291,7 @@ const MetadataEditor = createClass({
|
|||||||
onSelect={(value)=>this.handleLanguage(value)}
|
onSelect={(value)=>this.handleLanguage(value)}
|
||||||
onEntry={(e)=>{
|
onEntry={(e)=>{
|
||||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||||
debouncedHandleFieldChange('lang', e);
|
this.handleFieldChange('lang', e);
|
||||||
}}
|
}}
|
||||||
options={listLanguages()}
|
options={listLanguages()}
|
||||||
autoSuggest={{
|
autoSuggest={{
|
||||||
@@ -271,8 +299,7 @@ const MetadataEditor = createClass({
|
|||||||
clearAutoSuggestOnClick : true,
|
clearAutoSuggestOnClick : true,
|
||||||
filterOn : ['value', 'detail', 'title']
|
filterOn : ['value', 'detail', 'title']
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
</Combobox>
|
|
||||||
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
|
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -345,7 +372,7 @@ const MetadataEditor = createClass({
|
|||||||
placeholder='add tag' unique={true}
|
placeholder='add tag' unique={true}
|
||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}
|
onChange={(e)=>this.handleFieldChange('tags', e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
@@ -370,7 +397,7 @@ const MetadataEditor = createClass({
|
|||||||
values={this.props.metadata.invitedAuthors}
|
values={this.props.metadata.invitedAuthors}
|
||||||
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.']}
|
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)}
|
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>Privacy</h2>
|
<h2>Privacy</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
|
.userThemeName {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.metadataEditor {
|
.metadataEditor {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 5;
|
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
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.
|
||||||
@@ -71,8 +74,7 @@
|
|||||||
border : 1px solid gray;
|
border : 1px solid gray;
|
||||||
&:focus { outline : 1px solid #444444; }
|
&:focus { outline : 1px solid #444444; }
|
||||||
}
|
}
|
||||||
&.thumbnail {
|
&.thumbnail, &.themes{
|
||||||
height : 1.4em;
|
|
||||||
label { line-height : 2.0em; }
|
label { line-height : 2.0em; }
|
||||||
.value {
|
.value {
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
@@ -88,6 +90,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.themes{
|
||||||
|
.value {
|
||||||
|
overflow : visible;
|
||||||
|
text-overflow : auto;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.description {
|
&.description {
|
||||||
flex : 1;
|
flex : 1;
|
||||||
textarea.value {
|
textarea.value {
|
||||||
@@ -156,89 +169,73 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.themes.field {
|
.themes.field {
|
||||||
.navDropdownContainer {
|
& .dropdown-container {
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 100;
|
z-index : 100;
|
||||||
background-color : white;
|
background-color : white;
|
||||||
&.disabled {
|
}
|
||||||
font-style : italic;
|
& .dropdown-options {
|
||||||
color : dimgray;
|
overflow-y : visible;
|
||||||
background-color : darkgray;
|
}
|
||||||
}
|
.disabled {
|
||||||
& > div:first-child {
|
font-style : italic;
|
||||||
padding : 3px 3px;
|
color : dimgray;
|
||||||
background-color : inherit;
|
background-color : darkgray;
|
||||||
border : 1px solid gray;
|
}
|
||||||
i { float : right; }
|
.item {
|
||||||
&:hover {
|
position : relative;
|
||||||
color : white;
|
padding : 3px 3px;
|
||||||
background-color : @blue;
|
overflow : visible;
|
||||||
|
background-color : white;
|
||||||
|
border-top : 1px solid rgb(118, 118, 118);
|
||||||
|
.preview {
|
||||||
|
position : absolute;
|
||||||
|
top : 0;
|
||||||
|
right : 0;
|
||||||
|
z-index : 1;
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
width : 200px;
|
||||||
|
overflow : hidden;
|
||||||
|
color : black;
|
||||||
|
background : #CCCCCC;
|
||||||
|
border-radius : 5px;
|
||||||
|
box-shadow : 0 0 5px black;
|
||||||
|
opacity : 0;
|
||||||
|
transition : opacity 250ms ease;
|
||||||
|
h6 {
|
||||||
|
padding-block : 0.5em;
|
||||||
|
padding-inline : 1em;
|
||||||
|
font-weight : 900;
|
||||||
|
border-bottom : 2px solid hsl(0,0%,40%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navDropdown .item > p {
|
|
||||||
width : 45%;
|
.texture-container {
|
||||||
height : 1.1em;
|
position : absolute;
|
||||||
overflow : hidden;
|
top : 0;
|
||||||
text-overflow : ellipsis;
|
left : 0;
|
||||||
white-space : nowrap;
|
width : 100%;
|
||||||
}
|
height : 100%;
|
||||||
.navDropdown {
|
min-height : 100%;
|
||||||
position : absolute;
|
overflow : hidden;
|
||||||
width : 100%;
|
> img {
|
||||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
position : absolute;
|
||||||
.item {
|
top : 0;
|
||||||
position : relative;
|
right : 0;
|
||||||
padding : 3px 3px;
|
width : 50%;
|
||||||
overflow : visible;
|
min-height : 100%;
|
||||||
background-color : white;
|
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
border-top : 1px solid rgb(118, 118, 118);
|
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
.preview {
|
|
||||||
position : absolute;
|
|
||||||
top : 0;
|
|
||||||
right : 0;
|
|
||||||
z-index : 1;
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
width : 200px;
|
|
||||||
overflow : hidden;
|
|
||||||
color : black;
|
|
||||||
background : #CCCCCC;
|
|
||||||
border-radius : 5px;
|
|
||||||
box-shadow : 0 0 5px black;
|
|
||||||
opacity : 0;
|
|
||||||
transition : opacity 250ms ease;
|
|
||||||
h6 {
|
|
||||||
padding-block : 0.5em;
|
|
||||||
padding-inline : 1em;
|
|
||||||
font-weight : 900;
|
|
||||||
border-bottom : 2px solid hsl(0,0%,40%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
color : white;
|
|
||||||
background-color : @blue;
|
|
||||||
}
|
|
||||||
&:hover > .preview { opacity : 1; }
|
|
||||||
.texture-container {
|
|
||||||
position : absolute;
|
|
||||||
top : 0;
|
|
||||||
left : 0;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
min-height : 100%;
|
|
||||||
overflow : hidden;
|
|
||||||
> img {
|
|
||||||
position : absolute;
|
|
||||||
top : 0px;
|
|
||||||
right : 0;
|
|
||||||
width : 50%;
|
|
||||||
min-height : 100%;
|
|
||||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
|
||||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color : white;
|
||||||
|
background-color : @blue;
|
||||||
|
filter : unset;
|
||||||
|
}
|
||||||
|
&:hover > .preview { opacity : 1; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ module.exports = {
|
|||||||
(value)=>{
|
(value)=>{
|
||||||
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
|
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
theme: [
|
||||||
|
(value) => {
|
||||||
|
const URL = global.config.baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); //Escape any regex characters
|
||||||
|
const shareIDPattern = '[a-zA-Z0-9-_]{12}';
|
||||||
|
const shareURLRegex = new RegExp(`^${URL}\\/share\\/${shareIDPattern}$`);
|
||||||
|
const shareIDRegex = new RegExp(`^${shareIDPattern}$`);
|
||||||
|
if (value?.length === 0) return null;
|
||||||
|
if (shareURLRegex.test(value)) return null;
|
||||||
|
if (shareIDRegex.test(value)) return null;
|
||||||
|
|
||||||
|
return 'Must be a valid Share URL or a 12-character ID.';
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '10') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Looks like the brew you have selected
|
||||||
|
as a theme is not tagged for use as a
|
||||||
|
theme. Verify that
|
||||||
|
brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||||
|
{response.body.brewId}</a> has the <span className='lowercase'>meta:theme</span> tag!
|
||||||
|
</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'>
|
||||||
|
|||||||
@@ -379,7 +379,7 @@ const EditPage = createClass({
|
|||||||
const title = `${this.props.brew.title} ${systems}`;
|
const title = `${this.props.brew.title} ${systems}`;
|
||||||
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||||
|
|
||||||
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||||
},
|
},
|
||||||
@@ -410,7 +410,7 @@ const EditPage = createClass({
|
|||||||
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
||||||
view
|
view
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.publicUrl}/share/${shareLink}`);}}>
|
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
|
||||||
copy url
|
copy url
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
||||||
@@ -443,6 +443,7 @@ const EditPage = createClass({
|
|||||||
reportError={this.errorReported}
|
reportError={this.errorReported}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
userThemes={this.props.userThemes}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
snippetBundle={this.state.themeBundle.snippets}
|
||||||
updateBrew={this.updateBrew}
|
updateBrew={this.updateBrew}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
|
|||||||
@@ -168,6 +168,14 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Theme Not Valid
|
||||||
|
'10' : dedent`
|
||||||
|
## The selected theme is not tagged as a theme.
|
||||||
|
|
||||||
|
The brew selected as a theme exists, but has not been marked for use as a theme with the \`theme:meta\` tag.
|
||||||
|
|
||||||
|
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
|
||||||
|
|
||||||
//account page when account is not defined
|
//account page when account is not defined
|
||||||
'50' : dedent`
|
'50' : dedent`
|
||||||
## You are not signed in
|
## You are not signed in
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ const NewPage = createClass({
|
|||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
userThemes={this.props.userThemes}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
snippetBundle={this.state.themeBundle.snippets}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
onViewPageChange={this.handleEditorViewPageChange}
|
||||||
|
|||||||
@@ -552,6 +552,7 @@ const renderPage = async (req, res)=>{
|
|||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
publicUrl : config.get('publicUrl') ?? '',
|
publicUrl : config.get('publicUrl') ?? '',
|
||||||
|
baseUrl : `${req.protocol}://${req.get('host')}`,
|
||||||
environment : nodeEnv,
|
environment : nodeEnv,
|
||||||
deployment : config.get('heroku_app_name') ?? ''
|
deployment : config.get('heroku_app_name') ?? ''
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {model as HomebrewModel} from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
import GoogleActions from './googleActions.js';
|
import GoogleActions from './googleActions.js';
|
||||||
@@ -279,6 +279,8 @@ const api = {
|
|||||||
let currentTheme;
|
let currentTheme;
|
||||||
const completeStyles = [];
|
const completeStyles = [];
|
||||||
const completeSnippets = [];
|
const completeSnippets = [];
|
||||||
|
let themeName;
|
||||||
|
let themeAuthor;
|
||||||
|
|
||||||
while (req.params.id) {
|
while (req.params.id) {
|
||||||
//=== User Themes ===//
|
//=== User Themes ===//
|
||||||
@@ -292,6 +294,10 @@ const api = {
|
|||||||
|
|
||||||
currentTheme = req.brew;
|
currentTheme = req.brew;
|
||||||
splitTextStyleAndMetadata(currentTheme);
|
splitTextStyleAndMetadata(currentTheme);
|
||||||
|
if(!currentTheme.tags.some(tag => tag === "meta:theme" || tag === "meta:Theme"))
|
||||||
|
throw { brewId: req.params.id, name: 'Invalid Theme Selected', message: 'Selected theme does not have the meta:theme tag', status: 422, HBErrorCode: '10' };
|
||||||
|
themeName ??= currentTheme.title;
|
||||||
|
themeAuthor ??= currentTheme.authors?.[0];
|
||||||
|
|
||||||
// If there is anything in the snippets or style members, append them to the appropriate array
|
// If there is anything in the snippets or style members, append them to the appropriate array
|
||||||
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
||||||
@@ -301,6 +307,7 @@ const api = {
|
|||||||
req.params.renderer = currentTheme.renderer;
|
req.params.renderer = currentTheme.renderer;
|
||||||
} else {
|
} else {
|
||||||
//=== Static Themes ===//
|
//=== Static Themes ===//
|
||||||
|
themeName ??= req.params.id;
|
||||||
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
||||||
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
||||||
completeSnippets.push(localSnippets);
|
completeSnippets.push(localSnippets);
|
||||||
@@ -313,7 +320,9 @@ const api = {
|
|||||||
const returnObj = {
|
const returnObj = {
|
||||||
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
||||||
styles : completeStyles.reverse(),
|
styles : completeStyles.reverse(),
|
||||||
snippets : completeSnippets.reverse()
|
snippets : completeSnippets.reverse(),
|
||||||
|
name : themeName,
|
||||||
|
author : themeAuthor
|
||||||
};
|
};
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ brew`);
|
|||||||
describe('Theme bundle', ()=>{
|
describe('Theme bundle', ()=>{
|
||||||
it('should return Theme Bundle for a User Theme', async ()=>{
|
it('should return Theme Bundle for a User Theme', async ()=>{
|
||||||
const brews = {
|
const brews = {
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] }
|
||||||
};
|
};
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
@@ -587,6 +587,8 @@ brew`);
|
|||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
name : 'User Theme A',
|
||||||
|
author : 'authorName',
|
||||||
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
||||||
snippets : []
|
snippets : []
|
||||||
});
|
});
|
||||||
@@ -594,9 +596,9 @@ brew`);
|
|||||||
|
|
||||||
it('should return Theme Bundle for nested User Themes', async ()=>{
|
it('should return Theme Bundle for nested User Themes', async ()=>{
|
||||||
const brews = {
|
const brews = {
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
|
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style', tags: ['meta:theme'], authors: ['authorName'] }
|
||||||
};
|
};
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
@@ -607,6 +609,8 @@ brew`);
|
|||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
name : 'User Theme A',
|
||||||
|
author : 'authorName',
|
||||||
styles : [
|
styles : [
|
||||||
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||||
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||||
@@ -623,6 +627,8 @@ brew`);
|
|||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
name : '5ePHB',
|
||||||
|
author : undefined,
|
||||||
styles : [
|
styles : [
|
||||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
||||||
@@ -636,9 +642,9 @@ brew`);
|
|||||||
|
|
||||||
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
||||||
const brews = {
|
const brews = {
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
|
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style', tags: ['meta:theme'], authors: ['authorName'] }
|
||||||
};
|
};
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
@@ -649,6 +655,8 @@ brew`);
|
|||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
name : 'User Theme A',
|
||||||
|
author : 'authorName',
|
||||||
styles : [
|
styles : [
|
||||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
||||||
@@ -665,9 +673,9 @@ brew`);
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail for an invalid Theme in the chain', async()=>{
|
it('should fail for a missing Theme in the chain', async()=>{
|
||||||
const brews = {
|
const brews = {
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
|
||||||
};
|
};
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
@@ -686,6 +694,27 @@ brew`);
|
|||||||
name : 'ThemeLoad Error',
|
name : 'ThemeLoad Error',
|
||||||
status : 404 });
|
status : 404 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail for a User Theme not tagged with meta:theme', async ()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
await api.getThemeBundle(req, res)
|
||||||
|
.catch((e)=>err = e);
|
||||||
|
|
||||||
|
expect(err).toEqual({
|
||||||
|
HBErrorCode : '10',
|
||||||
|
brewId : 'userThemeAID',
|
||||||
|
message : 'Selected theme does not have the meta:theme tag',
|
||||||
|
name : 'Invalid Theme Selected',
|
||||||
|
status : 422 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteBrew', ()=>{
|
describe('deleteBrew', ()=>{
|
||||||
|
|||||||
@@ -44,13 +44,19 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{
|
|||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
obj.setState({ error: err });
|
obj.setState({ error: err });
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) {
|
||||||
|
obj.setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
|
themeBundle : {}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const themeBundle = res.body;
|
const themeBundle = res.body;
|
||||||
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
|
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
|
||||||
obj.setState((prevState)=>({
|
obj.setState((prevState)=>({
|
||||||
...prevState,
|
...prevState,
|
||||||
themeBundle : themeBundle
|
themeBundle : themeBundle,
|
||||||
|
error : null
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user