From 8bfa373da953788569a4c7adb92ad2c729af6247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 30 Sep 2026 21:40:37 +0200 Subject: [PATCH 01/12] refactor snippetbar into functional component --- .../homebrew/editor/snippetbar/snippetbar.jsx | 299 ++++++++---------- 1 file changed, 134 insertions(+), 165 deletions(-) diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index 9aa6b5ab7..dccfb1b4d 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -1,7 +1,6 @@ /*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/ import './snippetbar.less'; -import React from 'react'; -import createReactClass from 'create-react-class'; +import React, { useState, useEffect} from 'react'; import { Dropdown } from '@components/dropdown/dropdown.jsx'; import _ from 'lodash'; @@ -31,147 +30,122 @@ const execute = function(val, props){ return val; }; -const Snippetbar = createReactClass({ - displayName : 'SnippetBar', - getDefaultProps : function() { - return { - brew : {}, - view : 'text', - onViewChange : ()=>{}, - onInject : ()=>{}, - onToggle : ()=>{}, - showEditButtons : true, - renderer : 'legacy', - undo : ()=>{}, - redo : ()=>{}, - historySize : ()=>{}, - foldCode : ()=>{}, - unfoldCode : ()=>{}, - formatCode : ()=>{}, - cursorPos : {}, - themeBundle : [], - updateBrew : ()=>{} - }; - }, +const SnippetBar = ({ + brew = {}, + view = 'text', + onViewChange = ()=>{}, + onInject = ()=>{}, + onToggle = ()=>{}, + showEditButtons = true, + renderer = 'legacy', + undo = ()=>{}, + redo = ()=>{}, + historySize = ()=>{}, + foldCode = ()=>{}, + unfoldCode = ()=>{}, + formatCode = ()=>{}, + cursorPos = {}, + themeBundle = [], + updateBrew = ()=>{} +})=>{ + const [snippets, setSnippets] = useState([]); + const [showHistory, setShowHistory] = useState(false); + const [historyExists, setHistoryExists] = useState(false); + const [historyItems, setHistoryItems] = useState([]); - getInitialState : function() { - return { - renderer : this.props.renderer, - snippets : [], - showHistory : false, - historyExists : false, - historyItems : [] - }; - }, + useEffect(()=>{ + setSnippets(compileSnippets()); + }, []); - componentDidMount : async function(prevState) { - const snippets = this.compileSnippets(); - this.setState({ - snippets : snippets - }); - }, - - componentDidUpdate : async function(prevProps, prevState) { - if(prevProps.renderer != this.props.renderer || - prevProps.themeBundle != this.props.themeBundle || - prevProps.brew.snippets != this.props.brew.snippets) { - this.setState({ - snippets : this.compileSnippets() - }); - }; - - // Update history list if it has changed - const checkHistoryItems = await loadHistory(this.props.brew); - - // If all items have the noData property, there is no saved data - const checkHistoryExists = !checkHistoryItems.every((historyItem)=>{ - return historyItem?.noData; - }); - if(prevState.historyExists != checkHistoryExists){ - this.setState({ - historyExists : checkHistoryExists - }); - } - - // If any history items have changed, update the list - if(checkHistoryExists && checkHistoryItems.some((historyItem, index)=>{ - return index >= prevState.historyItems.length || !_.isEqual(historyItem, prevState.historyItems[index]); - })){ - this.setState({ - historyItems : checkHistoryItems - }); - } - }, - - mergeCustomizer : function(oldValue, newValue, key) { - if(key == 'snippets') { + const mergeCustomizer = (oldValue, newValue, key)=>{ + if(key == 'snippets') { const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme return result.filter((snip)=>snip.gen || snip.subsnippets); }; - }, - - compileSnippets : function() { + }; + const compileSnippets = ()=>{ let compiledSnippets = []; let oldSnippets = _.keyBy(compiledSnippets, 'groupName'); - if(this.props.themeBundle.snippets) { - for (let snippets of this.props.themeBundle.snippets) { + if(themeBundle.snippets) { + for (let snippets of themeBundle.snippets) { if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name snippets = ThemeSnippets[snippets]; const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName'); - compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer)); + compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, mergeCustomizer)); oldSnippets = _.keyBy(compiledSnippets, 'groupName'); } } - const userSnippetsasJSON = brewSnippetsToJSON(this.props.brew.title || 'New Document', this.props.brew.snippets, this.props.themeBundle.snippets); + const userSnippetsasJSON = brewSnippetsToJSON(brew.title || 'New Document', brew.snippets, themeBundle.snippets); compiledSnippets.push(userSnippetsasJSON); return compiledSnippets; - }, + }; - handleSnippetClick : function(injectedText){ - this.props.onInject(injectedText); - }, + useEffect(()=>{ + setSnippets(compileSnippets()); + }, [renderer, themeBundle, brew.snippets]); - renderSnippetGroups : function(){ - const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view); - if(snippets.length === 0) return null; + useEffect(()=>{ + const updateHistory = async ()=>{ + // Update history list if it has changed + const checkHistoryItems = await loadHistory(brew); + + // If all items have the noData property, there is no saved data + const checkHistoryExists = !checkHistoryItems.every( + (historyItem)=>historyItem?.noData + ); + + if(historyExists !== checkHistoryExists) { + setHistoryExists(checkHistoryExists); + } + + // If any history items have changed, update the list + if(!_.isEqual(checkHistoryItems, historyItems)) { + setHistoryItems(checkHistoryItems); + } + }; + + updateHistory(); + }, [brew]); + + const handleSnippetClick = (injectedText)=>onInject(injectedText); + + const renderSnippetGroups = ()=>{ + const currentSnippets = snippets.filter((snippetGroup)=>snippetGroup.view === view); + if(currentSnippets.length === 0) return null; return ; - }, + }; - replaceContent : function(item){ - return this.props.updateBrew(item); - }, + const replaceContent = (item)=>{ + return updateBrew(item); + }; - toggleHistoryMenu : function(){ - this.setState({ - showHistory : !this.state.showHistory - }); - }, + const toggleHistoryMenu = ()=>setShowHistory(!showHistory); - renderHistoryItems : function() { - if(!this.state.historyExists) return; + const renderHistoryItems = ()=>{ + if(!historyExists) return; return
- {_.map(this.state.historyItems, (item, index)=>{ + {_.map(historyItems, (item, index)=>{ if(item.noData || !item.savedAt) return; const saveTime = new Date(item.savedAt); @@ -185,106 +159,103 @@ const Snippetbar = createReactClass({ if(diffSecs > (24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (24 * 60 * 60))} days ago`; if(diffSecs > (7 * 24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (7 * 24 * 60 * 60))} weeks ago`; - return
{this.replaceContent(item);}} > + return
{replaceContent(item);}} > v{item.version} : {diffString}
; })}
; - }, + }; - renderEditorButtons : function(){ - if(!this.props.showEditButtons) return; + + const renderEditorButtons =()=>{ + if(!showEditButtons) return; return (
- {this.props.view !== 'meta' && this.props.view !== 'settings' && <>
- - -
- - -
}
- - - - -
); - }, + }; - render : function(){ - return
- {this.renderSnippetGroups()} - {this.renderEditorButtons()} -
; - } -}); + return
+ {renderSnippetGroups()} + {renderEditorButtons()} +
; +}; -export default Snippetbar; +export default SnippetBar; -const SnippetGroup = createReactClass({ - displayName : 'SnippetGroup', - getDefaultProps : function() { - return { - brew : {}, - groupName : '', - icon : 'fas fa-rocket', - snippets : [], - onSnippetClick : function(){}, - }; - }, - handleSnippetClick : function(e, snippet){ - this.props.onSnippetClick(execute(snippet.gen, this.props)); - }, - renderSnippets : function(snippets){ +const SnippetGroup = (props)=>{ + const { + brew = {}, + groupName = '', + icon = 'fas fa-rocket', + snippets = [], + onSnippetClick = function(){}, + } = props; + + const handleSnippetClick = (e, snippet)=>{ + onSnippetClick(execute(snippet.gen, props)); + }; + const renderSnippets = (snippets)=>{ return _.map(snippets, (snippet)=>{ if(!snippet.subsnippets){ return (
  • - +
  • + ); +} + // use setAttribute instead of the React style prop because React strips unknown CSS // properties (like anchor-name) from inline styles in browsers that don't support them. // setAttribute writes raw CSS text that the anchor positioning polyfill can read @@ -92,7 +107,7 @@ const Dropdown = ({ groupName, className = null, icon, children, color = null, c }; return ( -
  • +
  • +
  • + ); + })} + + ); }; - const renderEditorButtons =()=>{ if(!showEditButtons) return; return (
    {view !== 'meta' && view !== 'settings' && <>
    - + {renderHistoryMenu()} - - ); -} - // use setAttribute instead of the React style prop because React strips unknown CSS // properties (like anchor-name) from inline styles in browsers that don't support them. // setAttribute writes raw CSS text that the anchor positioning polyfill can read @@ -106,6 +91,21 @@ const Dropdown = ({ groupName, className = null, icon, children, color = null, c document.querySelectorAll('.menu-list:popover-open').forEach((openMenu)=>openMenu.hidePopover()); }; + if(disabled){ + return ( +
  • + +
  • + ); + } + return (