0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-25 07:33:00 +00:00

Compare commits

..

27 Commits

Author SHA1 Message Date
Víctor Losada Hernández
22e09b0879 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-11-19 11:00:02 +01:00
Víctor Losada Hernández
af477c56b1 last changes 2025-11-15 20:23:03 +01:00
Víctor Losada Hernández
4aacb36b3f Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-11-15 20:06:32 +01:00
Víctor Losada Hernández
dd63370d20 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-11-12 10:00:56 +01:00
Víctor Losada Hernández
9d72796a67 requested changes 2025-11-12 10:00:09 +01:00
Víctor Losada Hernández
6473ea571c Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-07-23 19:53:37 +02:00
Víctor Losada Hernández
2bcd317a4c reworked to be simpler 2025-07-23 19:52:05 +02:00
Víctor Losada Hernández
0ddca82c86 reorder one test 2025-07-18 16:43:49 +02:00
Víctor Losada Hernández
d5645083f3 remove prop drilling 2025-07-18 16:39:00 +02:00
Víctor Losada Hernández
e0bba53df1 Merge branch 'add-remove-author-if-owner' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-07-18 16:22:13 +02:00
Víctor Losada Hernández
223fc0a514 add prop again 2025-07-18 16:20:17 +02:00
Víctor Losada Hernández
625d30f3a8 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-07-18 16:19:42 +02:00
Trevor Buckner
6388cc7032 Merge branch 'master' into add-remove-author-if-owner 2025-07-17 14:03:04 -04:00
Víctor Losada Hernández
066de435d3 Merge branch 'master' into add-remove-author-if-owner 2025-06-02 12:42:04 +02:00
Víctor Losada Hernández
4ec6ea0f84 fix them? 2025-05-24 22:43:06 +02:00
Víctor Losada Hernández
a7f8ff5212 fix the damn thing 2025-05-24 22:35:29 +02:00
Víctor Losada Hernández
215abbf2f7 fix test 2025-05-24 22:31:11 +02:00
Víctor Losada Hernández
c005d4d387 change test name 2025-05-24 22:29:10 +02:00
Víctor Losada Hernández
fd38371eeb Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-05-24 22:14:47 +02:00
Víctor Losada Hernández
3c46312929 end tests? 2025-05-24 22:14:28 +02:00
Víctor Losada Hernández
55850f6d3c star tests 2025-05-20 11:39:22 +02:00
Víctor Losada Hernández
790bb5d1b7 lint 2025-05-20 11:39:16 +02:00
Víctor Losada Hernández
c51e8fd9d1 change api route to avoid collision 2025-05-13 08:23:23 +02:00
Víctor Losada Hernández
60714fbf58 lint 2025-05-12 12:27:17 +02:00
Víctor Losada Hernández
f040805d09 server side 2025-05-11 23:59:30 +02:00
Víctor Losada Hernández
c35138e7e3 client side 2025-05-11 23:59:25 +02:00
Víctor Losada Hernández
91f7d86fd4 prop drilling 2025-05-11 23:59:04 +02:00
9 changed files with 319 additions and 267 deletions

View File

@@ -47,6 +47,7 @@ const MetadataEditor = createClass({
getInitialState : function(){
return {
isOwner : global.account?.username && global.account?.username === this.props.metadata?.authors[0],
showThumbnail : true
};
},
@@ -156,6 +157,15 @@ const MetadataEditor = createClass({
});
},
handleDeleteAuthor : function(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)
});
},
renderSystems : function(){
return _.map(SYSTEMS, (val)=>{
return <label key={val}>
@@ -194,16 +204,54 @@ const MetadataEditor = createClass({
},
renderAuthors : function(){
let text = 'None.';
if(this.props.metadata.authors && this.props.metadata.authors.length){
text = this.props.metadata.authors.join(', ');
}
return <div className='field authors'>
<label>authors</label>
<div className='value'>
{text}
const authors = this.props.metadata.authors;
if(!this.state.isOwner || authors.length < 2) return (
<div className='field authors'>
<label>authors</label>
<div className='value'>
{authors.length > 0 && (
<a href={`/user/${authors[0]}`} className='author-link' title={`Owner - Click to open ${authors[0]}'s profile in a new tab`}>
{authors[0]}{authors.length > 1 && ', '}
</a>
)}
{authors.length > 1 && authors.slice(1).map((author, i)=>(
<a href={`/user/${author}`} className='author-link' title={`Author - Click to open ${author}'s profile in a new tab`}>
{author}{i+2 < authors.length && ', '}
</a>
))}
</div>
</div>
</div>;
);
return (
<div className='field authors'>
<label>Authors</label>
<ul className='list'>
{authors.length > 0 && (
<li className='tag owner' title='Owner'>
<a href={`/user/${authors[0]}`} className='author-link' title={`Owner - Click to open ${authors[0]}'s profile in a new tab`}>
{authors[0]}
</a>
</li>
)}
{authors.length > 1 && authors.slice(1).map((author, i)=>(
<li className='tag author' key={i + 1} title='Author'>
<a href={`/user/${author}`} className='author-link' title={`Author - Click to open ${authors[0]}'s profile in a new tab`}>
{author}
</a>
<button
onClick={()=>this.handleDeleteAuthor(author)}
className='delete'
title={`Remove ${author} as an author`}
>
<i className='fa fa-times fa-fw' />
</button>
</li>
))}
</ul>
</div>
);
},
renderThemeDropdown : function(){

View File

@@ -44,8 +44,6 @@
gap : 10px;
}
.field {
position : relative;
display : flex;
@@ -116,7 +114,6 @@
}
}
.thumbnail-preview {
position : relative;
flex : 1 1;
@@ -164,7 +161,47 @@
.colorButton(@red);
}
}
.authors.field .value { line-height : 1.5em; }
.authors.field {
.tag {
font-weight:300;
transition:background-color 0.2s;
&.owner {
position: relative;
background-color:@silverLight;
min-width:25px;
display:grid;
place-items:center;
font-weight: 900;
&::after {
content: "\f521";
font-family: "Font Awesome 6 Free";
color:gold;
position: absolute;
top: 0;
left: 0;
width:15px;
height:15px;
rotate:-45deg;
translate:-50% -50%;
}
}
&:has(button:hover) {
background:#d97d7d;
}
button {
color:@red;
}
}
a {
color:black;
text-decoration:unset;
}
}
.themes.field {
& .dropdown-container {
@@ -266,13 +303,17 @@
}
.tag {
padding : 0.3em;
padding : 0.35em;
margin : 2px;
font-size : 0.9em;
font-size : 0.95em;
background-color : #DDDDDD;
border-radius : 0.5em;
.icon { #groupedIcon; }
button {
cursor : pointer;
}
}
.input-group {

103
package-lock.json generated
View File

@@ -13,16 +13,16 @@
"@babel/core": "^7.28.4",
"@babel/plugin-transform-runtime": "^7.28.3",
"@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-react": "^7.27.1",
"@babel/runtime": "^7.28.4",
"@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^19.2.0",
"@googleapis/drive": "^18.0.0",
"@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
"cookie-parser": "^1.4.7",
"core-js": "^3.47.0",
"core-js": "^3.46.0",
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
@@ -34,29 +34,29 @@
"fs-extra": "11.3.2",
"hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2",
"js-yaml": "^4.1.1",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "15.0.12",
"marked-alignment-paragraphs": "^1.0.0",
"marked-definition-lists": "^1.0.1",
"marked-emoji": "^2.0.2",
"marked-emoji": "^2.0.1",
"marked-extended-tables": "^2.0.1",
"marked-gfm-heading-id": "^4.1.3",
"marked-gfm-heading-id": "^4.1.2",
"marked-nonbreaking-spaces": "^1.0.1",
"marked-smartypants-lite": "^1.0.3",
"marked-subsuper-text": "^1.0.4",
"marked-variables": "^1.0.4",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
"mongoose": "^8.20.0",
"mongoose": "^8.19.1",
"nanoid": "5.1.6",
"nconf": "^0.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
"react-router": "^7.9.6",
"react-router": "^7.9.4",
"romans": "^3.1.0",
"sanitize-filename": "1.6.3",
"superagent": "^10.2.1",
@@ -66,8 +66,8 @@
"devDependencies": {
"@stylistic/stylelint-plugin": "^4.0.0",
"babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.39.1",
"eslint-plugin-jest": "^29.1.0",
"eslint": "^9.37.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0",
"jest": "^30.2.0",
@@ -1427,9 +1427,9 @@
}
},
"node_modules/@babel/plugin-transform-react-display-name": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz",
"integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz",
"integrity": "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1795,14 +1795,14 @@
}
},
"node_modules/@babel/preset-react": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz",
"integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz",
"integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-transform-react-display-name": "^7.28.0",
"@babel/plugin-transform-react-display-name": "^7.27.1",
"@babel/plugin-transform-react-jsx": "^7.27.1",
"@babel/plugin-transform-react-jsx-development": "^7.27.1",
"@babel/plugin-transform-react-pure-annotations": "^7.27.1"
@@ -2258,9 +2258,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
"version": "9.39.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz",
"integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2295,9 +2295,9 @@
}
},
"node_modules/@googleapis/drive": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-19.2.0.tgz",
"integrity": "sha512-XzhQ+CNwW54umLecZLzQ5Q2RborJD1+u2YzJIT/BOpne+VJfsCyxWVG0nxYQmcGdiFXJPeW5+V3sdP/A/nt3eA==",
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-18.0.0.tgz",
"integrity": "sha512-nk4TirsHttwZSOjBEBjltCPDKUqwFso59G3WitNE+EGNVSVseSEq981f8Dmjq2ah0/fk3i206wuCU4PUCwcoTQ==",
"license": "Apache-2.0",
"dependencies": {
"googleapis-common": "^8.0.0"
@@ -5343,9 +5343,9 @@
}
},
"node_modules/core-js": {
"version": "3.47.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
"integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
"version": "3.46.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
@@ -6259,9 +6259,9 @@
}
},
"node_modules/eslint": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"version": "9.39.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz",
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6271,7 +6271,7 @@
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.1",
"@eslint/js": "9.39.0",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -6319,9 +6319,9 @@
}
},
"node_modules/eslint-plugin-jest": {
"version": "29.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.1.0.tgz",
"integrity": "sha512-LabxXbASXVjguqL+kBHTPMf3gUeSqwH4fsrEyHTY/MCs42I/p9+ctg09SJpYiD8eGaIsP6GwYr5xW6xWS9XgZg==",
"version": "29.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.0.1.tgz",
"integrity": "sha512-EE44T0OSMCeXhDrrdsbKAhprobKkPtJTbQz5yEktysNpHeDZTAL1SfDTNKmcFfJkY6yrQLtTKZALrD3j/Gpmiw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9466,9 +9466,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -10026,12 +10026,12 @@
}
},
"node_modules/marked-emoji": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.2.tgz",
"integrity": "sha512-EFnLQn4wTyf+6pXfptkm83Z2mt3VbdEYedHBAsDpwUas5n5satsj42RGqAijBpmetgGerI1EzUuzf7NIccINUQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.1.tgz",
"integrity": "sha512-P+nRr02dD+yPOFhtGdaVBzp0qzwlksI2f5GumIdHW/3UadzJ5sVi78CZikiSLr9PmdtUOZodZUBNIO6k38pDMQ==",
"license": "MIT",
"peerDependencies": {
"marked": ">=4 <18"
"marked": ">=4 <17"
}
},
"node_modules/marked-extended-tables": {
@@ -10044,15 +10044,15 @@
}
},
"node_modules/marked-gfm-heading-id": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-4.1.3.tgz",
"integrity": "sha512-aR0i63LmFbuxU/gAgrgz1Ir+8HK6zAIFXMlckeKHpV+qKbYaOP95L4Ux5Gi+sKmCZU5qnN2rdKpvpb7PnUBIWg==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-4.1.2.tgz",
"integrity": "sha512-EQ1WiEGHJh0C8viU+hbXbhHyWTDgEia2i96fiSemm2wdYER6YBw/9QI5TB6YFTqFfmMOxBFXPcPJtlgD0fVV2w==",
"license": "MIT",
"dependencies": {
"github-slugger": "^2.0.0"
},
"peerDependencies": {
"marked": ">=13 <18"
"marked": ">=13 <17"
}
},
"node_modules/marked-nonbreaking-spaces": {
@@ -10436,9 +10436,9 @@
}
},
"node_modules/mongoose": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.20.0.tgz",
"integrity": "sha512-SxqNb8yx+VOjIOx2l7HqkGvYuLC/T85d+jPvqGDdUbKJFz/5PVSsVxQzypQsX7chenYvq5bd8jIr4LtunedE7g==",
"version": "8.19.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.2.tgz",
"integrity": "sha512-ww2T4dBV+suCbOfG5YPwj9pLCfUVyj8FEA1D3Ux1HHqutpLxGyOYEPU06iPRBW4cKr3PJfOSYsIpHWPTkz5zig==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.4",
@@ -12065,9 +12065,9 @@
"license": "MIT"
},
"node_modules/react-router": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
"version": "7.9.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
"integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -14742,8 +14742,7 @@
},
"node_modules/vitreum": {
"version": "6.0.4",
"resolved": "git+https://git@github.com/calculuschild/vitreum.git#929c351881c4229550374421c7e2890a94f4dca7",
"integrity": "sha512-dclfZIgvZU4pOeju1U7CjfGcAMNY+HwhYxF5O1cB6j9EtBrUXQh5+8XlYvNrw2XIcjWIxWcWaCpAQgJxKssfbQ==",
"resolved": "git+https://git@github.com/calculuschild/vitreum.git#9d55fd6fb7e85e7070de798c4f9d5b983c1b7dba",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@@ -88,16 +88,16 @@
"@babel/core": "^7.28.4",
"@babel/plugin-transform-runtime": "^7.28.3",
"@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-react": "^7.27.1",
"@babel/runtime": "^7.28.4",
"@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^19.2.0",
"@googleapis/drive": "^18.0.0",
"@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
"cookie-parser": "^1.4.7",
"core-js": "^3.47.0",
"core-js": "^3.46.0",
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
@@ -109,29 +109,29 @@
"fs-extra": "11.3.2",
"hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2",
"js-yaml": "^4.1.1",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "15.0.12",
"marked-alignment-paragraphs": "^1.0.0",
"marked-definition-lists": "^1.0.1",
"marked-emoji": "^2.0.2",
"marked-emoji": "^2.0.1",
"marked-extended-tables": "^2.0.1",
"marked-gfm-heading-id": "^4.1.3",
"marked-gfm-heading-id": "^4.1.2",
"marked-nonbreaking-spaces": "^1.0.1",
"marked-smartypants-lite": "^1.0.3",
"marked-subsuper-text": "^1.0.4",
"marked-variables": "^1.0.4",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
"mongoose": "^8.20.0",
"mongoose": "^8.19.1",
"nanoid": "5.1.6",
"nconf": "^0.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
"react-router": "^7.9.6",
"react-router": "^7.9.4",
"romans": "^3.1.0",
"sanitize-filename": "1.6.3",
"superagent": "^10.2.1",
@@ -141,8 +141,8 @@
"devDependencies": {
"@stylistic/stylelint-plugin": "^4.0.0",
"babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.39.1",
"eslint-plugin-jest": "^29.1.0",
"eslint": "^9.37.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0",
"jest": "^30.2.0",

View File

@@ -6,7 +6,6 @@ import config from './config.js';
let serviceAuth;
let clientEmail;
if(!config.get('service_account')){
const reset = '\x1b[0m'; // Reset to default style
const yellow = '\x1b[33m'; // yellow color
@@ -16,10 +15,6 @@ if(!config.get('service_account')){
JSON.parse(config.get('service_account')) :
config.get('service_account');
if(keys?.client_email) {
clientEmail = keys.client_email;
}
try {
serviceAuth = googleDrive.auth.fromJSON(keys);
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
@@ -232,30 +227,14 @@ const GoogleActions = {
if(!obj) return;
if(clientEmail) {
await drive.permissions.create({
resource : {
type : 'user',
emailAddress : clientEmail,
role : 'writer'
},
fileId : obj.data.id,
fields : 'id',
})
.catch((err)=>{
console.log('Error adding Service Account permissions on Google Drive file');
console.error(err);
});
}
await drive.permissions.create({
resource : { type : 'anyone',
role : 'writer' },
role : 'writer' },
fileId : obj.data.id,
fields : 'id',
})
.catch((err)=>{
console.log('Error adding "Anyone" permissions on Google Drive file');
console.log('Error updating permissions');
console.error(err);
});

View File

@@ -171,7 +171,6 @@ const api = {
next();
};
},
getCSS : async (req, res)=>{
const { brew } = req;
if(!brew) return res.status(404).send('');
@@ -184,7 +183,6 @@ const api = {
});
return res.status(200).send(brew.style);
},
mergeBrewText : (brew)=>{
let text = brew.text;
if(brew.style !== undefined) {
@@ -202,7 +200,6 @@ const api = {
`${text}`;
return text;
},
getGoodBrewTitle : (text)=>{
const tokens = Markdown.marked.lexer(text);
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')

View File

@@ -204,7 +204,6 @@ describe('Tests for api', ()=>{
expect(id).toEqual('abcdefghij');
});
});
describe('getBrew', ()=>{
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
@@ -382,7 +381,68 @@ describe('Tests for api', ()=>{
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
});
});
describe('Get CSS', ()=>{
it('should return brew style content as CSS text', async ()=>{
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
expect(res.set).toHaveBeenCalledWith({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css'
});
});
it('should return 404 when brew has no style content', async ()=>{
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
it('should return 404 when brew does not exist', async ()=>{
const testBrew = { };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
});
describe('mergeBrewText', ()=>{
it('should set metadata and no style if it is not present', ()=>{
const result = api.mergeBrewText({
@@ -445,7 +505,6 @@ hello yes i am css
brew`);
});
});
describe('exclusion methods', ()=>{
it('excludePropsFromUpdate removes the correct keys', ()=>{
const sent = Object.assign({}, googleBrew);
@@ -483,7 +542,6 @@ brew`);
expect(result.pageCount).toBe(1);
});
});
describe('beforeNewSave', ()=>{
it('sets the title if none', ()=>{
const brew = {
@@ -525,7 +583,6 @@ brew`);
expect(hbBrew.text).toEqual('merged');
});
});
describe('newGoogleBrew', ()=>{
it('should call the correct methods', ()=>{
api.excludeGoogleProps = jest.fn(()=>'newBrew');
@@ -539,7 +596,6 @@ brew`);
expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
});
});
describe('newBrew', ()=>{
it('should set up a default brew via Homebrew model', async ()=>{
await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);
@@ -631,17 +687,6 @@ brew`);
});
});
});
describe('deleteGoogleBrew', ()=>{
it('should check auth and delete brew', async ()=>{
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
expect(result).toBe(true);
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
});
});
describe('Theme bundle', ()=>{
it('should return Theme Bundle for a User Theme', async ()=>{
const brews = {
@@ -785,7 +830,94 @@ brew`);
status : 422 });
});
});
describe('updateBrew', ()=>{
it('should return error on version mismatch', async ()=>{
const brewFromClient = { version: 1 };
const brewFromServer = { version: 1000, text: '' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
it('should return error on hash mismatch', async ()=>{
const brewFromClient = { version: 1, hash: '1234' };
const brewFromServer = { version: 1, text: 'test' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
// Commenting this one out for now, since we are no longer throwing this error while we monitor
// it('should return error on applying patches', async ()=>{
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
// const req = {
// brew : brewFromServer,
// body : brewFromClient,
// };
// let err;
// try {
// await api.updateBrew(req, res);
// } catch (e) {
// err = e;
// }
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
// });
it('should save brew, no ID', async ()=>{
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
model.save = jest.fn((brew)=>{return brew;});
const req = {
brew : brewFromServer,
body : brewFromClient,
query : { saveToGoogle: false, removeFromGoogle: false }
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({
_id : '1',
description : 'Test Description',
hash : '098f6bcd4621d373cade4e832627b4f6',
title : 'Test Title',
version : 2
})
);
});
});
describe('deleteGoogleBrew', ()=>{
it('should check auth and delete brew', async ()=>{
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
expect(result).toBe(true);
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
});
});
describe('deleteBrew', ()=>{
it('should handle case where fetching the brew returns an error', async ()=>{
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
@@ -1006,68 +1138,7 @@ brew`);
expect(saved.googleId).toEqual(brew.googleId);
});
});
describe('Get CSS', ()=>{
it('should return brew style content as CSS text', async ()=>{
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
expect(res.set).toHaveBeenCalledWith({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css'
});
});
it('should return 404 when brew has no style content', async ()=>{
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
it('should return 404 when brew does not exist', async ()=>{
const testBrew = { };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
});
describe('Split Text, Style, and Metadata', ()=>{
it('basic splitting', async ()=>{
@@ -1121,83 +1192,5 @@ brew`);
expect(testBrew.tags).toEqual(['tag a']);
});
});
describe('updateBrew', ()=>{
it('should return error on version mismatch', async ()=>{
const brewFromClient = { version: 1 };
const brewFromServer = { version: 1000, text: '' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
it('should return error on hash mismatch', async ()=>{
const brewFromClient = { version: 1, hash: '1234' };
const brewFromServer = { version: 1, text: 'test' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
// Commenting this one out for now, since we are no longer throwing this error while we monitor
// it('should return error on applying patches', async ()=>{
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
// const req = {
// brew : brewFromServer,
// body : brewFromClient,
// };
// let err;
// try {
// await api.updateBrew(req, res);
// } catch (e) {
// err = e;
// }
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
// });
it('should save brew, no ID', async ()=>{
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
model.save = jest.fn((brew)=>{return brew;});
const req = {
brew : brewFromServer,
body : brewFromClient,
query : { saveToGoogle: false, removeFromGoogle: false }
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({
_id : '1',
description : 'Test Description',
hash : '098f6bcd4621d373cade4e832627b4f6',
title : 'Test Title',
version : 2
})
);
});
});
});

View File

@@ -31,12 +31,7 @@ renderer.html = function (token) {
const openTag = html.substring(0, html.indexOf('>')+1);
html = html.substring(html.indexOf('>')+1);
html = html.substring(0, html.lastIndexOf('</div>'));
// Repeat the markdown processing for content inside the div, minus the preprocessing and postprocessing hooks which should only run once globally
const opts = Marked.defaults;
const tokens = Marked.lexer(html, opts);
Marked.walkTokens(tokens, opts.walkTokens);
return `${openTag} ${Marked.parser(tokens, opts)} </div>`;
return `${openTag} ${Marked.parse(html)} </div>`;
}
return html;
};

View File

@@ -492,7 +492,7 @@ body { counter-reset : page-numbers 0; }
.pageNumber { left : 30px; }
}
&:has(.resetCounting) { counter-set : page-numbers 1; }
.resetCounting { counter-set : page-numbers 1; }
&:not(:has(.skipCounting)) { counter-increment : page-numbers; }