-
Notifications
You must be signed in to change notification settings - Fork 140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Focus on expanded editor #1086
base: master
Are you sure you want to change the base?
Focus on expanded editor #1086
Conversation
src/reducers/ui.js
Outdated
|
||
function restoreFlexOnComponentToggle(componentName, state) { | ||
if (componentName === 'output') { | ||
return restoreDefaultRowFlex(state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't seem like the output component is ever hidden, but left this in here in case that's part of the roadmap. Happy to delete this functionality and simplify if that would be better!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, you’re right about that—it actually used to be possible, but the redesign didn’t have an output-minimization button, so I went ahead and dropped it from the initial implementation of the new layout. I think we could probably just put one in the preview top bar (next to either the refresh or the pop-out button) but also no one has mentioned missing it…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@YiningChen this is great! I am always happy to be brutal in code review but nothing to be brutal about here : )
Left a couple of minor style comments and raised one more substantive issue, but overall the approach here looks exactly right and is very nicely implemented!
src/reducers/ui.js
Outdated
@@ -24,6 +24,29 @@ const defaultState = new Immutable.Map(). | |||
set('topBar', new Immutable.Map({openMenu: null})). | |||
set('lastRefreshTimestamp', null); | |||
|
|||
// i.e. restore to before editors were resized |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m generally not a fan of most “explanatory” comments—it’s very easy for them to diverge from the code they seek to explain, and if variables/functions/classes are named carefully, I don’t think comments are usually necessary. In this case I think the function names restoreDefaultColumnFlex
etc. are entirely self-documenting!
src/reducers/ui.js
Outdated
} | ||
return state.setIn(['workspace', 'columnFlex'], DEFAULT_COLUMN_FLEX); | ||
state = focusEditor( | ||
action.payload.componentName.replace('editor.', ''), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely the most low-impact way to do what we need to do here, but I’m not in love with the idea of parsing a string that’s just being used as an internal identifier. Seems like it might be better to change the structure of the action a bit so that the information we need is discretely available. Maybe something like {componentType: 'editor', componentId: 'javascript'}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay! That makes a lot of sense! Thanks :)
src/reducers/ui.js
Outdated
return state.setIn(['workspace', 'columnFlex'], DEFAULT_COLUMN_FLEX); | ||
state = focusEditor( | ||
action.payload.componentName.replace('editor.', ''), | ||
state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very minor nitpick, but project style would be to put the )
on the next line : )
heyyy @YiningChen any idea when we might be able to get this puppy over the finish line?? it’s so close! |
ah!! Sorry @outoftime had a busy couple of weeks 😱, but planning on picking it back up tomorrow! |
@YiningChen no worries, totally understand! can’t wait to ship it! |
src/reducers/projects.js
Outdated
@@ -88,6 +92,7 @@ export function reduceRoot(stateIn, action) { | |||
|
|||
export default function reduceProjects(stateIn, action) { | |||
let state; | |||
const {payload} = action; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this to get past the line length linter 😂 But let me know if this would cause issues!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a policy of not really nitpicking the reducers until after I am able to refactor them to have a reasonable structure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would actually say this is a good change on the merits—in fact, I’d probably argue for just fully destructuring action
in the params list itself!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! Right that totally makes sense! thanks!
src/util/componentName.js
Outdated
return componentType; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple extra lines of whitespace here
src/util/componentName.js
Outdated
@@ -0,0 +1,8 @@ | |||
export function makeComponentName({componentType, componentId}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@YiningChen can you share the reasoning for having this? In particular, why not just make the {componentType, componentId}
structure the thing we use universally to describe a UI component, rather than still using the string representation in some cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No good reasoning! I assumed there was already a reason it was a string, which is a bad reason xD Will change, thanks!
src/reducers/projects.js
Outdated
@@ -88,6 +92,7 @@ export function reduceRoot(stateIn, action) { | |||
|
|||
export default function reduceProjects(stateIn, action) { | |||
let state; | |||
const {payload} = action; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would actually say this is a good change on the merits—in fact, I’d probably argue for just fully destructuring action
in the params list itself!
@YiningChen getting verrrrrrry close i think! sorry about the annoying component structure thing, that gives this a much bigger footprint than it would have otherwise, but i still think it’s good! |
No worries! It's been fun digging into more and more as well! :D |
@YiningChen would love to ship this! it’s very close! now that the Britney Pickle Spears have been served, think you might have a chance to bring it home? |
lol! I have a working version that refactored every call to show/hide actions, but ran into a weird bug that I think was related to a previous saved project's schema not matching up to the new schema (where hiddenUIComponents is an array of objects instead of a set of strings). Seemed to work after forcing it to be an array with lodash's concat, but wanted to play around with it a bit more to see if there's a better solution! |
@YiningChen gotcha! i figured that kind of issue would come up sooner or later (actually surprised it wasn’t sooner)—I think the move is probably to introduce the concept of “migrations” that would be applied to projects right when we dehydrate them from Firebase. Basically the idea I had in mind was to create a collection of functions, each of which takes a project, checks it for a particular outdated structure (in this case, strings for hidden components), and then “migrates” it to the newer structure. As we make more changes to how projects are structured, we’d just add more functions to the list. Does that seem reasonable? |
Oooo okay! yeah definitely! 😄 I'll aim to get that done latest soon after thanksgiving! :) |
@YiningChen exciting! I think I’ll probably have time to really dig into this Wednesday but just perusing it a bit, I think it would be really helpful if you could update the PR description with a high-level outline of the code changes here—in particular I’m not quite grasping the idea of the “hidden component line” data structure and how it fits into the overall change we’re making here. Can’t wait to dive in deeper! |
(Also it looks like there’s a conflict with master right now so would be great to resolve that if you have a chance) |
Ah yes! sorry! will do! |
src/components/Editor.jsx
Outdated
@@ -58,13 +58,15 @@ class Editor extends React.Component { | |||
} | |||
|
|||
componentWillUnmount() { | |||
const {onComponentHidden, language} = this.props; | |||
const {row, column} = this._editor.getCursorPosition(); | |||
onComponentHidden(language, row, column); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if there is a better place for this action to be called, I would have ideally liked to call it from EditorContainer.jsx
, where the hide function is called.
@YiningChen great writeup! looking forward to digging into this 😀 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@YiningChen OK! I’ve just gone through and gotten good and familiar with the changes here.
Holding off on line-level feedback for now as I wanted to bring up one high-level thought. As I understand it, the basic sequence of events that happens when you hide an editor component is:
- Action dispatched to Redux requesting that editor be hidden
- Redux updates store to mark the editor as hidden
- React components update, causing editor component to unmount
componentWillUnmount
lifecycle hook on editor component dispatches an action to Redux declaring, with its last breath, “this is the row and column that was focused”- Redux store handles this action, annotating the “hidden component” object with the row/column information
- When it’s time to show the editor component again, the UI dispatches a
focusLine
action which includes the row/column metadata stored in step 5
Assuming I’m not totally off-base here, I am a little uneasy about the above, for a handful of related reasons:
- It feels weird to dispatch a Redux action from a
componentWillUnmount
lifecycle hook. In general I expect the UI (React component hierarchy) to change in response to changes in the Redux store, not vice versa. - More simply, any time we are dispatching multiple actions to fully handle a single user action, it’s a sign that something may be amiss. We should be able to dispatch a single action that says “the user did X” and the reducers/sagas should be able to make updates accordingly.
- The
HiddenComponent
record type hasline
andcolumn
properties, which are only meaningful for one special case of hidden component. - As a corollary to the multiple-actions observation above, actions should also straightforwardly describe what happened—in this case we are using an action that encodes “the user asked for this line to be focused”, but in reality the user simply asked for the component to be displayed. This may be a simple matter of naming though.
Taking a step back, I would propose that we’re really dealing with two distinct pieces of information:
- What components are currently visible?
- What is the user’s cursor position within each source in the current project?
Looking at it that way, I think the most straightforward way of modeling that information would be to store the cursor positions in their own place in the ui
state, updating it every time the user moves the cursor or changes the selection. This may seem crazy but we already update the state every time the user presses a key, so it’s safe to say there are no rules.
Under this regime the <Editor>
component would be responsible not only for updating the cursor position in the Redux store, but also for consuming that information and updating the Ace editor’s internal cursor position to match received props.
One advantage of this proposal is that it would obviate the rather tortured requestedFocusedLine
approach that has always felt like a bit of a kludge—a click on an error, for instance, would straightforwardly update UI state saying “this is where the cursor should be now”.
That said: the whole idea of restoring the exact cursor position when a hidden element is un-hidden is a lovely UX flourish but feels like something that could absolutely happen in a followup pull request. I’m more than happy to work with you on finding the best possible way to do that in the context of this PR, but I am also totally down to punt on it in service of getting the core of this enhancement released sooner. There’s a lot to like here!
I am also, as always, 100% down to be convinced that everything I’ve wrote here is misguided and the current approach is in fact the best solution for the problem!!
Let me know your thoughts!
Hi Matt! That definitely makes a lot sense, and it didn't feel 100% right when I called that action from Also out of curiosity, how come the editors are destroyed and recreated when toggled as opposed to using CSS to hide them? I would love to follow up this in the future, and the best implementation I can think of to avoid storing line/column would to just refocus the editor using Thanks for the thorough review! :) |
@YiningChen great question/thought. Off the top of my head, I can’t recall any specific intent behind adding/removing the editors from the DOM vs. just hiding them with CSS. The latter certainly seems worth exploring! |
…ch renaming of onEditorHide
…her functions passed from props
currentProject.hiddenUIComponents, | ||
`editor.${language}`, | ||
), | ||
language => find(currentProject.hiddenUIComponents, {language}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@outoftime My only hesitation with renaming the language
property in HiddenUIComponent
to instance
is this line, since I really like the how the language
property connects to how language is used in this file. i.e. language
is more specific about the piece of information it's storing. instance
also wouldn't be used by anything that isn't an editor, and I can't imagine we'd want multiple consoles or instructions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel you, but I think the decoupling of the property from the specific use case here is valuable enough to make it worth it. And I think passing the search spec {instance: language}
here would actually be nicely self-documenting regarding the relationship between the two.
@@ -63,8 +63,7 @@ class Editor extends React.Component { | |||
} | |||
|
|||
_focusRequestedLine(requestedFocusedLine) { | |||
if (get(requestedFocusedLine, 'component') !== | |||
`editor.${this.props.language}`) { | |||
if (get(requestedFocusedLine, 'componentKey') !== this.props.language) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't feel 100% right here to use language as the componentKey
, but I wanted to get rid of all editor.${this.props.language}
, which also feels a bit arbitrary. Please let me know if you have better ideas!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it seems like the structure of HiddenUIComponent
could do the work here too—basically this is another case where we are referring to a specific UI component instance in the context of the Redux store. That would argue for giving HiddenUIComponent
a more general name, something like UIComponentInstance
(or something more poetic), but I think that would be a good change anyway since there’s nothing about that record that is tied to the concern of hiding/showing. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(The same observation would of course apply here as well.)
ready for review again! |
@YiningChen awesome! will look at this as soon as i can! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@YiningChen hey this is looking really good!! left a bunch of comments but largely driving at one high-level thing which is that I think there is enough overlap between the concept of a componentKey
and a HiddenUIComponent
record that we should consolidate them (and prefer the structure of HiddenUIComponent
, though probably with a more generalized name).
Let me know if what I’ve written here makes sense and whether you agree!
); | ||
|
||
export const toggleComponent = createAction( | ||
'TOGGLE_COMPONENT', | ||
(projectKey, componentName) => ({projectKey, componentName}), | ||
(_projectKey, _componentName, timestamp = Date.now()) => ({timestamp}), | ||
(projectKey, componentKey, hiddenUIComponent) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the hiddenUIComponent
parameter here is a vestige of an earlier approach?
@@ -106,7 +106,8 @@ function buildGistFromProject(project) { | |||
language: 'Markdown', | |||
}; | |||
} | |||
if (project.enabledLibraries.length || project.hiddenUIComponents.length) { | |||
if (project.enabledLibraries.length || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the change to isEmpty
on the next line, let’s do it here too?
@@ -63,8 +63,7 @@ class Editor extends React.Component { | |||
} | |||
|
|||
_focusRequestedLine(requestedFocusedLine) { | |||
if (get(requestedFocusedLine, 'component') !== | |||
`editor.${this.props.language}`) { | |||
if (get(requestedFocusedLine, 'componentKey') !== this.props.language) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it seems like the structure of HiddenUIComponent
could do the work here too—basically this is another case where we are referring to a specific UI component instance in the context of the Redux store. That would argue for giving HiddenUIComponent
a more general name, something like UIComponentInstance
(or something more poetic), but I think that would be a good change anyway since there’s nothing about that record that is tied to the concern of hiding/showing. WDYT?
@@ -63,8 +63,7 @@ class Editor extends React.Component { | |||
} | |||
|
|||
_focusRequestedLine(requestedFocusedLine) { | |||
if (get(requestedFocusedLine, 'component') !== | |||
`editor.${this.props.language}`) { | |||
if (get(requestedFocusedLine, 'componentKey') !== this.props.language) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(The same observation would of course apply here as well.)
currentProject.hiddenUIComponents, | ||
`editor.${language}`, | ||
), | ||
language => find(currentProject.hiddenUIComponents, {language}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel you, but I think the decoupling of the property from the specific use case here is valuable enough to make it worth it. And I think passing the search spec {instance: language}
here would actually be nicely self-documenting regarding the relationship between the two.
this.props.dispatch( | ||
hideComponent( | ||
this.props.currentProject.projectKey, | ||
componentName, | ||
language, | ||
new HiddenUIComponent({componentType: 'editor', language}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Building on some of my previous feedback, it seems like we’re duplicating a bit of information here (specifically language
) and could broadly be more consistent about how we’re referencing UI components. In particular, I think the properties of HiddenUIComponent
—a componentType
and optional instance
—comprise the right way to describe a UI component in all situations that we need to:
- Hide/unhide component
- Request focused line
- Maybe others I am not thinking of?
So my proposal would be that action creators for the above always take a componentType
and optional instance
, and the resulting structures in state always use a HiddenUIComponent
record. WDYT?
@@ -12,23 +31,25 @@ export default class Project extends Record({ | |||
projectKey: null, | |||
sources: new Sources(), | |||
enabledLibraries: new Set(), | |||
hiddenUIComponents: new Set(['console']), | |||
hiddenUIComponents: new Map({ | |||
console: new HiddenUIComponent({componentType: 'console'}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the more I look at this the more I think that the componentKey
idea isn’t actually getting us anything, and is just a sort of lossy alternate representation of the information in the HiddenUIComponent
record. My pitch would be to get rid of the componentKey
altogether and always just refer to components using the collection of properties contained in HiddenUIComponent
.
In order to do what we want to do here, I would suggest making this a Map
where the keys are HiddenUIComponent
records, and the values are booleans, with true
meaning the component should be hidden, and false
meaning it should be visible. Or we could rename it to something like componentVisibility
and invert the meaning of the booleans. In the case where a map key is not present at all, we’d take that as “do the default” for that component (currently, show everything except the console).
import {Record} from 'immutable'; | ||
|
||
export default Record({ | ||
componentType: '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably just make this default to null
too?
obj = hiddenUIComponents; | ||
} else if (isArray(hiddenUIComponents)) { | ||
let component; | ||
for (component of hiddenUIComponents) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about for (const component of hiddenUIComponents)
?
components => components.add('console'), | ||
), | ||
); | ||
return state.set(project.projectKey, Project.fromJS(project)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
@YiningChen any chance you have the bandwidth to pick this up and bring it to the finish line? Happy to help out with merge conflicts or if you’re too busy, I can take over, just let me know! |
ah sorry I totally forgot about this for awhile! started a new job and moved across the country xD I should have time this weekend! but if not totally fine with it being taken over :) Thanks! |
Oh wow congrats! No wild rush at all, just wanted to see if you were up for finishing it : ) |
Focuses editor when user expands a collapsed editor! (Issue #1080)
hiddenUIComponents
, I turned it from a Set to a Map, with the key being referenced ascomponentKey
in parameters. (before it was sometimes calledcomponentName
and other times justcomponent
)editor.${language}
, as "editor" is stored ascomponentType
for each hidden component.HiddenUIComponent
record)Also, as I was working I was wondering if instead of
hiddenUIComponents
in state, it could just beUIComponents
with ahidden
property. It might also make it easier to tell if new components like the console and instructions were purposefully expanded, or if they just didn't exist for old projects.