diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b02835726..020653272 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,26 +1,29 @@ - +> [!TIP] +> Before submitting a Pull Request, please consider the following to speed up reviews: +> - 👷‍♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs. +> - 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first. +> - 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests? +> - 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding. ## Description +_Describe what your PR accomplishes. Consider walking through the main changes to aid reviewers in following your code, especially if it covers multiple files._ ## Related Issues or Discussions +> [!CAUTION] +> If no issue exists yet, create it, and get agreement on the approach (or paste in a previous agreement from chat, etc.) before moving forward. (Experimental PRs are OK without prior discussion, but do not expect to get merged.) + - Closes # ## QA Instructions, Screenshots, Recordings -_Please replace this line with instructions on how to test or view your changes, as well as any before/after -images for UI changes._ +_Replace this line with instructions on how to test or view your changes, as well as any before/after +screenshots or recordings for UI changes._ ### Reviewer Checklist -_Please replace the list below with specific features you want reviewers to look at._ +_Replace the list below with specific features you want reviewers to look at._ *Reviewers, refer to this list when testing features, or suggest new items * - [ ] Verify new features are functional @@ -32,5 +35,3 @@ _Please replace the list below with specific features you want reviewers to look - [ ] Feature A handles negative numbers - [ ] Identify opportunities for simplification and refactoring - [ ] Check for code legibility and appropriate comments - -
Copy this list diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.less b/client/homebrew/brewRenderer/toolBar/toolBar.less index c2f4ff148..c787a6f6b 100644 --- a/client/homebrew/brewRenderer/toolBar/toolBar.less +++ b/client/homebrew/brewRenderer/toolBar/toolBar.less @@ -115,10 +115,10 @@ color : #D3D3D3; accent-color : #D3D3D3; - &::-webkit-slider-thumb, &::-moz-slider-thumb { + &::-webkit-slider-thumb, &::-moz-range-thumb { width : 5px; height : 5px; - cursor : pointer; + cursor : ew-resize; outline : none; } diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index 50237b914..f7d9508f8 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -207,19 +207,11 @@ const Snippetbar = createClass({ renderEditorButtons : function(){ if(!this.props.showEditButtons) return; - const foldButtons = <> -
- -
-
- -
- ; + - return
-
+ return ( +
+ {this.props.view !== 'meta' && <>
@@ -235,14 +227,21 @@ const Snippetbar = createClass({
- {foldButtons} -
+ +
+
+ +
+
{this.state.themeSelector && this.renderThemeSelector()}
-
- +
} +
-
; +
+ ) }, render : function(){ diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less index 319cd0cad..7d56dc718 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.less +++ b/client/homebrew/editor/snippetbar/snippetbar.less @@ -22,7 +22,7 @@ justify-content : flex-end; min-width : 225px; - &:only-child { margin-left : auto; } + &:only-child { margin-left : auto;min-width:unset;} >div { display : flex; @@ -38,6 +38,11 @@ line-height : @menuHeight; text-align : center; cursor : pointer; + + &.editorTool:not(.active) { + cursor:not-allowed; + } + &:hover,&.selected { background-color : #999999; } &.text { .tooltipLeft('Brew Editor'); diff --git a/package-lock.json b/package-lock.json index 3a4fc600c..257045fc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@babel/core": "^7.26.0", "@babel/plugin-transform-runtime": "^7.25.9", "@babel/preset-env": "^7.26.0", - "@babel/preset-react": "^7.25.9", + "@babel/preset-react": "^7.26.3", "@googleapis/drive": "^8.14.0", "body-parser": "^1.20.2", "classnames": "^2.5.1", @@ -21,9 +21,9 @@ "cookie-parser": "^1.4.7", "create-react-class": "^15.7.0", "dedent-tabs": "^0.10.3", - "dompurify": "^3.2.2", + "dompurify": "^3.2.3", "expr-eval": "^2.0.2", - "express": "^4.21.1", + "express": "^4.21.2", "express-async-handler": "^1.2.0", "express-static-gzip": "2.2.0", "fs-extra": "11.2.0", @@ -39,7 +39,7 @@ "marked-smartypants-lite": "^1.0.2", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", - "mongoose": "^8.8.3", + "mongoose": "^8.8.4", "nanoid": "5.0.9", "nconf": "^0.12.1", "react": "^18.3.1", @@ -1661,9 +1661,10 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.9.tgz", - "integrity": "sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.26.3.tgz", + "integrity": "sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-option": "^7.25.9", @@ -5461,10 +5462,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz", - "integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==", - "license": "(MPL-2.0 OR Apache-2.0)", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", + "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -6253,9 +6253,10 @@ "license": "MIT" }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -6276,7 +6277,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -6291,6 +6292,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-async-handler": { @@ -10877,9 +10882,9 @@ } }, "node_modules/mongoose": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.3.tgz", - "integrity": "sha512-/I4n/DcXqXyIiLRfAmUIiTjj3vXfeISke8dt4U4Y8Wfm074Wa6sXnQrXN49NFOFf2mM1kUdOXryoBvkuCnr+Qw==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.4.tgz", + "integrity": "sha512-yJbn695qCsqDO+xyPII29x2R7flzXhxCDv09mMZPSGllf0sm4jKw3E9s9uvQ9hjO6bL2xjU8KKowYqcY9eSTMQ==", "license": "MIT", "dependencies": { "bson": "^6.7.0", @@ -11742,9 +11747,10 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", diff --git a/package.json b/package.json index ccc557f75..17d1b5533 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@babel/core": "^7.26.0", "@babel/plugin-transform-runtime": "^7.25.9", "@babel/preset-env": "^7.26.0", - "@babel/preset-react": "^7.25.9", + "@babel/preset-react": "^7.26.3", "@googleapis/drive": "^8.14.0", "body-parser": "^1.20.2", "classnames": "^2.5.1", @@ -93,9 +93,9 @@ "cookie-parser": "^1.4.7", "create-react-class": "^15.7.0", "dedent-tabs": "^0.10.3", - "dompurify": "^3.2.2", + "dompurify": "^3.2.3", "expr-eval": "^2.0.2", - "express": "^4.21.1", + "express": "^4.21.2", "express-async-handler": "^1.2.0", "express-static-gzip": "2.2.0", "fs-extra": "11.2.0", @@ -111,7 +111,7 @@ "marked-smartypants-lite": "^1.0.2", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", - "mongoose": "^8.8.3", + "mongoose": "^8.8.4", "nanoid": "5.0.9", "nconf": "^0.12.1", "react": "^18.3.1", diff --git a/server/googleActions.js b/server/googleActions.js index ec541e5f5..2c2cbac73 100644 --- a/server/googleActions.js +++ b/server/googleActions.js @@ -241,8 +241,8 @@ const GoogleActions = { return obj.data.id; }, - getGoogleBrew : async (id, accessId, accessType)=>{ - const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth }); + getGoogleBrew : async (auth = defaultAuth, id, accessId, accessType)=>{ + const drive = googleDrive.drive({ version: 'v3', auth: auth }); const obj = await drive.files.get({ fileId : id, diff --git a/server/homebrew.api.js b/server/homebrew.api.js index b8d4024ad..a75887742 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -87,76 +87,68 @@ const api = { // Create middleware with the accessType passed in as part of the scope return async (req, res, next)=>{ // Get relevant IDs for the brew - const { id, googleId } = api.getId(req); + let { id, googleId } = api.getId(req); const accessMap = { edit : { editId: id }, share : { shareId: id }, - admin : { - $or : [ - { editId: id }, - { shareId: id }, - ] } + admin : { $or : [{ editId: id }, { shareId: id }] } }; // Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine. let stub = await HomebrewModel.get(accessMap[accessType]) .catch((err)=>{ - if(googleId) { + if(googleId) console.warn(`Unable to find document stub for ${accessType}Id ${id}`); - } else { + else console.warn(err); - } }); stub = stub?.toObject(); + googleId ??= stub?.googleId; + + const isOwner = stub?.authors?.length === 0 || stub?.authors?.[0] === req.account?.username; + const isAuthor = stub?.authors?.includes(req.account?.username); + const isInvited = stub?.invitedAuthors?.includes(req.account?.username); + + if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) { + const accessError = { name: 'Access Error', status: 401, authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId }; + if(req.account) + throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' }; + else + throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04' }; + } if(stub?.lock?.locked && accessType != 'edit') { throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title }; } // If there is a google id, try to find the google brew - if(!stubOnly && (googleId || stub?.googleId)) { - let googleError; - const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType) - .catch((err)=>{ - googleError = err; + if(!stubOnly && googleId) { + const oAuth2Client = isOwner? GoogleActions.authCheck(req.account, res) : undefined; + + const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType) + .catch((googleError)=>{ + const reason = googleError.errors?.[0].reason; + if(reason == 'notFound') + throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username }; + else + throw { ...googleError, HBErrorCode: '01' }; }); - // Throw any error caught while attempting to retrieve Google brew. - if(googleError) { - const reason = googleError.errors?.[0].reason; - if(reason == 'notFound') { - throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username }; - } else { - throw { ...googleError, HBErrorCode: '01' }; - } - } + // Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew; } - const authorsExist = stub?.authors?.length > 0; - const isAuthor = stub?.authors?.includes(req.account?.username); - const isInvited = stub?.invitedAuthors?.includes(req.account?.username); - if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) { - const accessError = { name: 'Access Error', status: 401 }; - if(req.account){ - throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId }; - } - throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId }; - } // If after all of that we still don't have a brew, throw an exception - if(!stub && !stubOnly) { + if(!stub) throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id }; - } // Clean up brew: fill in missing fields with defaults / fix old invalid values - if(stub) { - stub.tags = stub.tags || undefined; // Clear empty strings - stub.renderer = stub.renderer || undefined; // Clear empty strings - stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields - } + stub.tags = stub.tags || undefined; // Clear empty strings + stub.renderer = stub.renderer || undefined; // Clear empty strings + stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields - req.brew = stub ?? {}; + req.brew = stub; next(); }; }, diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index 814db26f8..8270b1568 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -298,7 +298,7 @@ describe('Tests for api', ()=>{ expect(next).toHaveBeenCalled(); expect(api.getId).toHaveBeenCalledWith(req); expect(model.get).toHaveBeenCalledWith({ shareId: '1' }); - expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share'); + expect(google.getGoogleBrew).toHaveBeenCalledWith(undefined, '2', '1', 'share'); }); it('access is denied to a locked brew', async()=>{ diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less index cb73b0a88..84a5c63f1 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.less +++ b/shared/naturalcrit/codeEditor/codeEditor.less @@ -11,49 +11,54 @@ @import (less) './themes/fonts/iconFonts/fontAwesome.less'; @keyframes sourceMoveAnimation { - 50% {background-color: red; color: white;} - 100% {background-color: unset; color: unset;} + 50% { color : white;background-color : red;} + 100% { color : unset;background-color : unset;} } -.codeEditor{ - @media screen and (pointer : coarse) { - font-size : 16px; - } - .CodeMirror-foldmarker { - font-family: inherit; - text-shadow: none; - font-weight: 600; - color: grey; - } +.codeEditor { + @media screen and (pointer : coarse) { + font-size : 16px; + } + .CodeMirror-foldmarker { + font-family : inherit; + font-weight : 600; + color : grey; + text-shadow : none; + } - .sourceMoveFlash .CodeMirror-line{ - animation-name: sourceMoveAnimation; - animation-duration: 0.4s; - } + .CodeMirror-foldgutter { + cursor : pointer; + border-left : 1px solid #EEEEEE; + transition : background 0.1s; + &:hover { background : #DDDDDD; } + } - .CodeMirror-vscrollbar { - &::-webkit-scrollbar { - width: 20px; - } - &::-webkit-scrollbar-thumb { - width: 20px; - background: linear-gradient(90deg, #858585 15px, #808080 15px); - } - } + .sourceMoveFlash .CodeMirror-line { + animation-name : sourceMoveAnimation; + animation-duration : 0.4s; + } + + .CodeMirror-vscrollbar { + &::-webkit-scrollbar { width : 20px; } + &::-webkit-scrollbar-thumb { + width : 20px; + background : linear-gradient(90deg, #858585 15px, #808080 15px); + } + } - //.cm-tab { - // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; - //} + //.cm-tab { + // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; + //} - //.cm-trailingspace { - // .cm-space { - // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right; - // } - //} + //.cm-trailingspace { + // .cm-space { + // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right; + // } + //} } .emojiPreview { - font-size: 1.5em; - line-height: 1.2em; + font-size : 1.5em; + line-height : 1.2em; } \ No newline at end of file diff --git a/shared/naturalcrit/styles/core.less b/shared/naturalcrit/styles/core.less index 21a2687bc..3248269c5 100644 --- a/shared/naturalcrit/styles/core.less +++ b/shared/naturalcrit/styles/core.less @@ -43,5 +43,6 @@ html,body, #reactRoot{ } &:disabled{ background-color : @silver !important; + cursor:not-allowed; } }