-
Notifications
You must be signed in to change notification settings - Fork 524
Updates continue/submit button text in lab2. #69085
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
Conversation
06982bf to
384924b
Compare
e1f255d to
36bff36
Compare
36bff36 to
bc8e5ca
Compare
fisher-alice
left a comment
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.
Nice! Just left some questions, one about about shared UI with GuideInstructions. Thanks for taking this on!
| "submitAnswer": "Submit answer", | ||
| "submitAssessment": "Submit your assessment", | ||
| "submitSurvey": "Submit your survey", | ||
| "submitWork": "Submit Work", |
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.
Just curious - do we still need to add loc strings?
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 do so here because this is a global element that is visible in legacy applications... though, it is still unlikely to get a translation. We might go in and manually add a manual translation for a few languages, however. For instance, a spanish translation we manually add seems appropriate for this. This is follow-up work.
| state => state.lab.validationState?.satisfied | ||
| ); | ||
| // Get the level number of the next level (in the progression) | ||
| const continueToLevel = useAppSelector( |
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.
Nit: could name something like nextLevelNumber and currentLevel as currentLevelNumber (below).
| * | ||
| * Bubble choice levels return the parent level in the case where the navigation | ||
| * type is not 'NEXT_LEVEL', since that is where the progression would take them | ||
| * when they press 'continue' in that case. |
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.
Could you add in the doc what happens when type is NEXT_LEVEL and not PARENT? And to confirm my understanding, would the level after the parent level be returned? If so, would 'Continue to Level X' be displayed?
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.
Also, what happens if the bubble_choice level is last in the progression with NEXT_LEVEL navigation type? Would 'Finish Lesson' be displayed?
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.
Tagging @sanchitmalhotra126 who added navigation_type in #69083 - it seems like if the sublevel navigation type is NEXT_LEVEL, then the level after parent level would be returned, and if the parent level was the last in a progression, undefined would be returned?
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 correct! If getNextLevel returns undefined, in either case, we will show the "Finish" text rather than Continue.
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.
Nit: could update to include both navigation types:
* Bubble choice levels return the parent level in the case where the navigation type is 'PARENT' and not 'NEXT_LEVEL'
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.
That's technically true, however, it's a default behavior where NEXT_LEVEL indicates breaking the default behavior. It's documenting the guard and not the else statement: Here's what to do to influence the default behavior for sublevels... which usually go to the parent level when you are done. I don't want to over-document in case anything is updated where the documentation could go out of sync... though to be honest a good way to deal with that is a test and we have none for these. The proper thing to do would be to add some tests to ensure we capture the documented behavior. I will do 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.
Thanks Wilkie for your thoughtful response - this makes sense!
| "continueBeyondHourOfCode": "Continue Beyond an Hour of Code", | ||
| "continueLesson": "Continue lesson", | ||
| "continueToActivity": "Continue to activity", | ||
| "continueToLevel": "Continue to Level {level}", |
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.
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.
And do you mind checking levels in music lab? They share the navigation area, but their navigation button is not pinned to the bottom as it is in other lab2 labs. It appears once the required actions are taken by user. Thanks!
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.
Good find. Yes, these buttons are problematic when localized too since being too long is difficult to accommodate.
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.
Resolved this by using a resize observer. If the button is too small for whatever reason, it will render the simpler text we were using before.
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.
Please see the video in the PR description.
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.
Thanks for adding the variant prop!
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.
Updated to just truncate the text with text-overflow so it shows ... (although another update just used the added variant prop to force Guide components to use the old text)
| "findYourSchool": "Find your school or provide details if it's not listed.", | ||
| "finish": "Finish", | ||
| "finishCreatingSections": "Finish creating sections", | ||
| "finishLesson": "Finish Lesson", |
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.
Similar impact on Guide UI - probably need to check if folks want 'Finish Lesson' instead of 'Finish' on last level of Hour of AI.
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.
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, so if there is only one lesson? That's an interesting question.
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.
Resolved this by investigating the number of lessons in the script. If there's only one, then we always just say 'Finish' instead of 'Finish Lesson'.
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.
Nice!
48962cc to
5e52153
Compare
f91e344 to
a858e22
Compare
🖼️ Storybook Visual Comparison Report✅ No Storybook eyes differences detected! |
|
|
||
| .navigationFooter { | ||
| padding: 10px; | ||
| background-color: var(--background-neutral-secondary); |
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 is part of this ticket to update the background color to have it stand out against feedback messages in the instructions panel... so that the button doesn't look like it is relating to any feedback or messaging that might appear directly above it.
| useEffect(() => { | ||
| const container = buttonContainerRef.current; | ||
|
|
||
| let resizeObserver: ResizeObserver | undefined; |
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.
When I load a level, the full button text is shown, but when I go to next level, shortened version if shown. But full text is shown if I reload level.
I think that we're missing dependency on level id?
Screen.Recording.2025-12-11.at.3.38.40.PM.mov
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 element gets unmounted and hidden for a moment as the level switches leading it to have a 0 width for a frame. However, it doesn't trigger the event again when it is revealed. It's a little haphazard and mysterious, though it is fixed if you ignore 0 width... I've elected to abandon it and just do normal text truncation since the main issue was with Guide components and we just force those to render the old text anyway.
fisher-alice
left a comment
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.
Thanks so much for iterating on this! We discussed a couple possible solutions to the resizing issue on Slack so will wait to see what you decide on for that.
| styleAsBubble?: boolean; | ||
| /** Optional on continue/finish callback. */ | ||
| onContinue?: () => void; | ||
| /** Whether or not to keep the text 'simple' */ |
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.
Nit: could rename to something like buttonTextVariant.
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.
+1, or something like simpleButtonText?: boolean? variant feels a bit generic here
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 dislike boolean properties! What if you want to add a third option? You'd have to rewrite the whole property. I learned this lesson from a W3C CSS editor when he asked me why I thought there were no boolean properties in CSS: because you don't want to be stuck with it. Doesn't 100% apply here of course... we can change them, but it's on my mind a whole dang lot.
variant is a conventional React property for influencing the appearance where it doesn't necessarily affect the behavior of a component. It felt right to use it when it is a property that decides a certain type of rendering behavior. It is meant to be generic. It's possible we might want a third option at some point.
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.
That's fair! I do still think a more specific property name would be helpful though as NavigationArea refers to more than just the navigation button.
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.
Thanks Wilkie for sharing your thoughts! I agree with Sanchit that a more specific property name would be helpful, like buttonTextSize or buttonTextVariant since NavigationArea is used in InstructionsV2 and the resource panel which both contain a long list of props, some of which get passed to NavigationArea.
But I'm good with your keeping variant if you feel strongly about it. Thanks for the discussion!
| "continueBeyondHourOfCode": "Continue Beyond an Hour of Code", | ||
| "continueLesson": "Continue lesson", | ||
| "continueToActivity": "Continue to activity", | ||
| "continueToLevel": "Continue to Level {level}", |
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.
Thanks for adding the variant prop!
| * | ||
| * Bubble choice levels return the parent level in the case where the navigation | ||
| * type is not 'NEXT_LEVEL', since that is where the progression would take them | ||
| * when they press 'continue' in that case. |
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.
Nit: could update to include both navigation types:
* Bubble choice levels return the parent level in the case where the navigation type is 'PARENT' and not 'NEXT_LEVEL'
| const smallText = hasNextLevel | ||
| ? commonI18n.continue() | ||
| : lessonCount > 1 | ||
| ? commonI18n.finishLesson() |
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 wonder for the 'small' variant if we just want to always display 'Finish'? @dju90 curious for your thoughts here.
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. Makes sense. Done.
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.
Sorry, what's the alternative?
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 was suggesting that If the button text variant is 'small' (as it is in the GuideInstructions for the Mix and Move progresion), the text will always be 'Finish', even if there is more than one lesson in the course unit.
The alternative would be that even if the button text variant is 'small', that 'Finish lesson' would be displayed if there is more than one lesson.
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.
Oh I see. Okay yes, having just "Finish" always for the small variant sounds good to me.
| "findYourSchool": "Find your school or provide details if it's not listed.", | ||
| "finish": "Finish", | ||
| "finishCreatingSections": "Finish creating sections", | ||
| "finishLesson": "Finish Lesson", |
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.
Nice!
Ensures small variant uses 'Finish' instead of 'Finish Lesson'.
fisher-alice
left a comment
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.
🎉 - thanks for addressing my earlier questions/comments - looks great!
Just left a non-blocking suggestion on updating NavigationArea prop name. Thanks Wilkie!
| ); | ||
| } | ||
|
|
||
| return; |
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.
nit: remove?
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.
That's not a nit. That's just wrong!!
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.
Just curious - Is it wrong or is it not necessary/redundant?
| styleAsBubble?: boolean; | ||
| /** Optional on continue/finish callback. */ | ||
| onContinue?: () => void; | ||
| /** Whether or not to keep the text 'simple' */ |
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.
+1, or something like simpleButtonText?: boolean? variant feels a bit generic here
| const nextLevelNumber = useAppSelector( | ||
| state => getNextLevel(state)?.levelNumber | ||
| ); | ||
| // Get the current main level (parent if it is a sublevel) | ||
| const currentLevel = useAppSelector( | ||
| state => | ||
| getParentLevel(state)?.levelNumber || getCurrentLevel(state)?.levelNumber | ||
| ); |
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.
Optional: could move the logic for computing the button text into one selector instead of pulling out these two values separately
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 generally dislike adding logic to the redux selectors, but I'm happy to do 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.
Yeah I suggest this just because then technically the component will re-render only when the actual text value changes, rather than when either of these values change.
|
Screenshots look good to me! Thank you @wilkie and @fisher-alice |
| state.progress.currentLessonId, | ||
| currentLevel.parentLevelId | ||
| ); | ||
| currentLevelIndex = parentLevel.levelNumber - 1; |
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.
What's confusing is why this is levelNumber - 1 to get to the level after the parent level. I'd expect just levelNumber.
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.
But it is because the levelNumber is 1 based and we need to convert it to a zero-based index.
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.
Although if it has an unplugged activity, all this logic already breaks down unless the level data specifically has title || position in such a way that it provides the right 0-based index. hm.
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.
One of my favorite unplugged lessons is Binary Bracelets. Looking at the script, I see
position is a Script Level property that gets assigned tolevelNumber on the frontend I think?
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.
Indeed. I love that we promote unplugged and more offline content.
Here's the logic (processedLevel()) that decides the levelNumber... indeed it is decided on the frontend and is an interpretation of the data sent from the backend. So, the level data contains a levelNumber that is undefined if unplugged and otherwise is the title value (falling back to the position if title is false-y (0 or undefined))
code-dot-org/apps/src/templates/progress/progressHelpers.js
Lines 285 to 288 in 8f96a57
| levelNumber: | |
| level.kind === LevelKind.unplugged | |
| ? undefined | |
| : level.title || level.position, |
So if you follow the logic through, when it constructs the level array that it is using to dictate the level progression, it does this by calling levelsByLesson() which maps each level via levelWithProgress():
code-dot-org/apps/src/code-studio/progressReduxSelectors.js
Lines 242 to 251 in 8f96a57
| lessons.map(lesson => | |
| lesson.levels.map(level => { | |
| let statusLevel = levelWithProgress( | |
| {levelResults, unitProgress, levelPairing, currentLevelId}, | |
| level, | |
| lesson.lockable | |
| ); | |
| return statusLevel; | |
| }) | |
| ); |
Which always expands the level data via that aforementioned processedLevel method:
code-dot-org/apps/src/code-studio/progressReduxSelectors.js
Lines 174 to 180 in 8f96a57
| const levelWithProgress = ( | |
| {levelResults, unitProgress, levelPairing = {}, currentLevelId}, | |
| level, | |
| isLockable, | |
| parentLevelId | |
| ) => { | |
| const normalizedLevel = processedLevel(level, parentLevelId); |
So, if you have level data (like the test data being used in progressReduxTest.js) that is [{kind: 'unplugged', title: 'Unplugged'}, {title: 1}, {title: 2}] you'll get a levels array that is: [{levelNumber: undefined}, {levelNumber: 1}, {levelNumber: 2}] which then breaks the logic because if you are on the second level which has levelNumber equal to 1, it will decide that the current level index is 0 (not 1) and advance to level "index" 1, which is the level it is already on.
I don't know why this isn't a problem elsewhere. Probably because the test data is not very representative? It says that the test data is real data, though. But I would assume that the title is somehow accurate to level "index" in real-life situations.
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.
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.
Well, it would be the same as the level index + 1, but yes. In the test data, it would render in the progress bubbles as (Unplugged Activity) (1) (2) which would break the expected behavior. This is a tangent of the original intention of this task... but to add tests, I needed to fix it to work for the test case. Overall, it breaking in the test case makes me nervous regardless. Let's see if I can prove that this never actually happens in the wild.
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.
In practice, title is not a real field. It is computed relative to position:
| title: level_display_text, |
code-dot-org/dashboard/app/models/script_level.rb
Lines 337 to 345 in 1bf0ffc
| def level_display_text | |
| if level.unplugged? | |
| I18n.t('unplugged_activity') | |
| elsif lesson.unplugged_lesson? | |
| position - 1 | |
| else | |
| position | |
| end | |
| end |
It does seem like a lesson with an unplugged activity as level 1 would break the progression logic for the continue button on any level in that lesson... if the subsequent levels have their positions be 2, 3, 4, etc? title is not a very good representation of levelNumber in this case... or the logic to produce the levels array should ignore the unplugged levels (where levelNumber === undefined).
Searching for this:
[development] dashboard > ls=Lesson.all.filter{|lesson| lesson.unplugged_lesson?}
[development] dashboard > ls_of_interest=ls.filter{|lesson| lesson.script_levels.second&.level_display_text == 1}
[development] dashboard > ls_of_interest.map(&:script).map(&:name).uniq
=> ["AlgebraB", "algebra", "course1", "course2", "course3", "k5concepts"]
So all deprecated courses and k5concepts which doesn't use the progression logic.
It just so happens we used course3 as the test example for the progress redux when it's the rare example of a course that breaks the progress logic. Neat.
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.
TIL the difference between an unplugged level and an external level displayed as an unplugged activity. Also learned more about levelNumber and title and see that the Unplugged Activity bubble label is actually set by whether level.isUnplugged is true or not in BubbleFactory.jsx (not by levelNumber).
Nice investigation! I see that lesson.unplugged_lesson? only returns true if the first level (or oldest active level) is a true unplugged level:
code-dot-org/dashboard/app/models/lesson.rb
Lines 156 to 160 in cf22a17
| def unplugged_lesson? | |
| script_level = get_script_level_by_id | |
| return false if script_level.blank? | |
| script_level.oldest_active_level.unplugged? | |
| end |
So it makes sense that the title or level_display_text in modern courses including those with unplugged activities are assigned position (and not position - 1).
Here’s an example of a modern course with an unplugged activity as the first level and the second level as bubble 2. https://studio.code.org/courses/coursea-2025/units/1/lessons/11/levels/1


As you found, only deprecated courses uses the position - 1 for title. You can see the second level has bubble 1 in the screenshot the deprecated course 3 lesson 1 progression.
I wonder if maybe in a follow-up we should replace the test data in progressReduxTest with more modern course data ('coursea' instead of 'course3')?
Learned a ton - thanks Wilkie!
|
|
||
| const currentLevelIndex = levels.findIndex( | ||
| level => level.levelNumber === currentLevelNumber | ||
| ); |
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 logic to come up with the index into the levels array was updated since when I wrote the unit tests, the test data actually doesn't work with the old logic. The test data has an unplugged activity at the beginning of the lesson (which is actually somewhat typical) and this gets index 0 with the lesson after it having title: 1, which made it levelNumber: 1. The old logic always assumed that levelNumber was 1 greater than that level's index into the levels array (so it believed it would be entry 0, but it's not... it's entry 1). The old logic had the test lesson's level 1 going to itself as the next level in a loop.
This logic still works for the cases that fit that assumption but now also work for other bizarre cases where levels without numbers might appear. Although I considered finding currentLevelNumber + 1, this would not handle level progressions where the levelNumbers weren't perfectly ordered and wouldn't match existing behavior. Old and new behaviors will still go to an unnumbered level if that still gets placed into the levels array via our lesson processing logic.
| }); | ||
| }); | ||
|
|
||
| describe('nextLevelId', () => { |
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.
Nit: add unit test for when on the last level of a progression.
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.
That's not a nit! That's an actual omission.
| }); | ||
| }); | ||
|
|
||
| describe('getParentLevel', () => { |
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.
thank you for adding these unit tests! 🎉
| ); | ||
| currentLevelIndex = parentLevel.levelNumber - 1; | ||
| // So, we just consider the 'current' level the current parent | ||
| currentLevelNumber = parentLevel.levelNumber; |
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.
Nice update!



Does the following:
...since the text is significantly longervariantprop that can restore the old text and behavior which is used on the Hour of AIGuideelements.Also done: "Submit" becomes "Submit Work" but I do not know about a lab2 assessment level to illustrate it.
Width Constraints
Sometimes the button is just too small for the full 'Continue to Level X' text, so it might collapse with
...:2025-12-12.14-37-24.mp4
Notes
This updates the progress redux selectors since we need to know the level number to render the 'Continue to Level X' text and there's no easy way to get the level data. The existing code was only to support getting the level id for the next level for navigation and to know if there's a next level. So this adds the
getParentLevelandgetNextLevelconvenience functions and refactors a bit when those functions could be used internally.Links
PR Creation Checklist: