Home page / multiple projects - #1333
Draft
microbit-matt-hillsdon wants to merge 6 commits into
Draft
microbit-matt-hillsdon wants to merge 6 commits into
microbit-matt-hillsdon wants to merge 6 commits into
Conversation
* Let FileSystem switch to a different storage Groundwork for a project list: opening a project will swap the FileSystem's backing storage rather than replace files one by one. switchStorage waits for any in-flight initialisation, makes the new storage the record, refills the hex file system from it and bumps the version of every file it holds so an editor showing a same-named file reloads it. Versions increment rather than reset because the editor keys on name plus version; a reset to 1 could collide with the old project's version and leave stale content on screen. * Persist the current project in an IndexedDB project database The editor's file system is now mirrored to IndexedDB rather than session storage, as the first step towards a library of projects. Behaviour is unchanged for the user: one project per tab, and a new tab gets a new project. - projects-db.ts: the database. Two stores, project metadata and files keyed by [projectId, name], so listing is cheap and a change writes only the files it touched. The database name includes the base path because production, beta and review builds share an origin. Opening asserts the stores exist so an old build fails clearly against a database a newer one created. - indexeddb-storage.ts: an FSStorage for one project, used as the SplitStrategyStorage secondary. Keystroke writes are coalesced per file and flushed in one transaction after a short delay and when the page is hidden. A failed flush is reported, not thrown, and its changes dropped: the in-memory primary still has them and retrying forever against a full quota helps nobody. - current-project.ts: which project a tab opens. The tab's current project id lives in session storage. A session-storage file system from before this change is migrated into the library on first load, so a reload after deploy lands in the user's work; only the file system keys are removed, since session settings live there too. Without IndexedDB, or with an incompatible database, the editor falls back to session storage as before. - SplitStrategyStorage accepts a promise of its secondary, since opening IndexedDB is asynchronous and the Host API is not. The shared FSStorage tests move to storage-tests.ts so the new storage runs them too, against fake-indexeddb. * Share one project library across review builds Review builds are internal, and a project made on one branch is useful on the next, so they share a database rather than each having their own by base path. Production and beta keep separate libraries. The cost is that a schema change can leave the shared library incompatible with an older build. On non-public stages that now shows a "Breaking change to stored data" page offering to clear the database and reload, as ml-trainer does, instead of quietly falling back to session storage. Public stages keep the quiet fallback. The outcome of opening the database is held in a small external store, since storage opens at module load before React mounts. * Drop the dirty flag from the projects database and its before-unload prompt The dirty flag means "changed since the last hex save" and exists only to warn before work is lost: the before-unload prompt and the replace-project confirmation are its only readers. Neither applies to a project in the database, which outlives the tab, so the flag is no longer stored there and IndexedDBFSStorage always reports not dirty. Session storage keeps it and FileSystem still tracks it for the tab, so the replace confirmation works for edits made in this tab until the replace flows go. openCurrentProjectStorage now reports through storage-status when the projects database is active, and BeforeUnloadDirtyCheck registers nothing in that case. The session-storage fallback and iframe mode keep the prompt. The edits e2e spec now asserts the prompt is absent and that text typed just before a reload survives via the pagehide flush. The old helper proved nothing: Playwright accepts an unlistened beforeunload dialog itself, so the new one listens for it. * Add e2e coverage for the session-storage fallback and iframe controller mode The three storage modes now behave differently and only the projects database path was exercised in the browser. storage-errors.test.ts uses a new noIndexedDB fixture option, an init script that hides indexedDB before the app loads, and checks the fallback keeps the before-unload prompt and survives a reload. iframe.test.ts embeds the editor with controller=1 and checks the workspacesync, workspaceloaded, workspacesave and importproject messages and the prompt. The host page is an HTML string in the spec, served by intercepting a request for it, so it is never part of the build.
This is a first cut, ported from ml-trainer by extracting shared components that now live in the ui project. Lots still remains to agree and implement tracked in docs/home-page.md that will be resolved before this lands on main. Detail from squash: * Take @microbit/ui-carousel for the home and projects pages Adds the carousel package and its swiper peer, includes its source in the Panda extraction, and extends dev:link-ui so a local ../ui checkout supplies it alongside ui and ui-patterns. * Move the editor to /project The home and projects pages take the root, as decided in the multi-project plan. Documentation links to the editor's old /:tab/:slug paths redirect to the same tab under /project; a #project: link at the root redirects to the editor with its hash, since microbit.org pages still link there. Anything else is the shared not-found page. Iframe controller mode keeps the editor at the root as its only route: the embedding page owns the URL and there are no other pages. The mode is provided through context so the URL builders pick the right shape. The host strips a #project: hash from the current URL rather than the boot URL, which no longer names the page the editor is on after the redirect. * Choose the editor's project when its route loads Projects lists, creates, opens, renames, duplicates and deletes projects and keeps the file system on the open one. The host now takes the file system's persistent storage as a promise that Projects resolves when a project is first opened, so the pages can render without choosing one; later opens go through FileSystem.switchStorage. The database opens at boot and reports itself active; the editor route's loader asks Projects for a project, sharing one choice between concurrent calls. The tab's project is chosen as before, then the most recent project rather than a new one, so a straight-to-editor bookmark reopens your work. A new project is created with the starter program so it can be opened by switching, which does not run the host's initialisation. Pending writes are stamped with the edit time rather than the flush time, so "last modified" orders projects by when they were edited. Flushes report a change and the IndexedDB storage no longer closes the shared connection. Tabs tell each other about changes over a BroadcastChannel; a tab with the changed project open reloads it, or lets go of a deleted one. The router is created once at module level: a browser router runs its loaders on creation and StrictMode's double render made two, each creating a project. * Add the home and projects pages Composed from @microbit/ui-patterns' project components and @microbit/ui-carousel, with app-local layout, banner, resource cards and what each action does. The home page shows the banner, the user's projects with a new-project card and a link to see them all, and rows of project ideas, teacher resources and help. The projects page has search, sort, multi-select and a toolbar that slides in from the bottom on narrow screens. Both take their list from the route loader, so there is no empty flash, and stay current through Projects' change events. Without the projects database the pages redirect to the editor, which is then the whole app. A #project: link at the root goes to the editor with its hash. Opening a project resets the editor's file to main.py. Theme images resolve per file, as in ml-trainer, so the branded package only ships what differs; the OSS defaults are neutral placeholders. Project management events follow ml-trainer's names and are documented. The carousel's catalog joins the compiled messages. * Add a home button to the editor's sidebar header Shown only when the projects database is active and not in iframe mode, where there is no home page to go to. * Add e2e coverage for the home and projects pages and the new routes Page objects for the home and projects pages, with the card and menu helpers shared between them, and specs for creating, opening, renaming, duplicating and deleting projects from cards and the toolbar, search and sort, the legacy documentation redirects, not-found, a #project: link at the root, and a new tab opening the most recent project. * Make the sidebar logo the way home instead of a separate button With the sidebar collapsed there is no room for a home button beside the logo, so the logo links to the home page when the projects database is active outside iframe mode. Otherwise it keeps its microbit.org link, as the fallback and iframe cases have no home page to go to. Short term; revisit with the header. E2e gets an App.goHome helper for the link. * Show the Python logo in brand colour at the top-left of project cards ProjectIcon mirrors ml-trainer's component of the same name. On the projects page it drops below the selection checkbox's 60px hover strip so the two do not crowd the corner; the home row gets a matching inset. The theme's spacing scale is smaller than Panda's default, so the margins were measured in the browser rather than taken from ml-trainer. * Make a #project: link a new project when the projects database is active PendingMigration holds the link the app booted with and hands it out once. The Projects session takes it when the editor first chooses a project and creates one from it, ahead of the tab's current project, so a link no longer overwrites whatever was open. Without the database the host takes it as before and writes it into the single implicit project. The editor route's loader redirects off the hash once the link has become a project; the session cannot strip it itself because the router commits the navigation, hash included, after the loader returns. * Import files as new projects, and add files to the open project A hex is a whole program, so it always becomes a new project, whether dropped on the editor, chosen from the Files tab, or imported from the home page. Other files join the open project from the editor, replacing any with the same names, and become a new project from the home page, where a single script becomes main.py and names the project. Ideas from the documentation open as new projects too. Without the projects database a hex or idea replaces the single implicit project, with no confirmation. ProjectImporter (src/project/project-import.tsx) holds this for both surfaces. The confirm-replace dialog, the choose-main-script dialog and the replace-or-add flow go, along with Reset project; replacing a program now means importing from the home page. The Project tab is Files, Open is Add files and Open leaves the action bar. FileSystem.filesFromHex reads a hex with a separate file system so the open project is untouched; the MicroPython hex is fetched once for both. project_import gains a surface param; project_reset is retired. * Align the home page projects row with ml-trainer Use the solid button variant for Import, rename the row to "My projects" with a "View all" link, and add the info tooltip explaining that projects are stored in the browser. The tooltip text is copied from ml-trainer. Tidying the catalogs also removes translations of strings that an earlier commit dropped from English, which the CI tidy check requires. * Let resource cards fill their carousel slide The carousel fixes slides at 260px. Copying ml-trainer's token width gave 225px under this app's dense preset, leaving slack inside every slide so the resource rows looked more widely spaced than the projects row. * Link the private theme package with a script and align home page content with ml-trainer Add dev:link-theme, which builds ../python-editor-v3-microbit and symlinks it into node_modules, so the branded home page is the routine local view. Rename the help row to "Help" and point the banner's Learn more at the brand's user guide link, hidden when a deployment has none. * Show a persistent toast when saving to the projects database fails A failed flush raises a saveerror event on the Projects session, logged to Sentry once per session because writes are per keystroke. The toast and its strings are ml-trainer's so Crowdin dedupes them: a quota error says browser storage is full, anything else that the save failed. Page actions that hit the quota show the same toast; their other failures keep the generic unexpected-error toast, since "failed to save" would misdescribe a failed delete or rename. * Show the organisation logo and Python Editor wordmark in the page header, and drop file names from project cards BrandConfig gains AppLogo and OrgLogo with ml-trainer's LogoProps shape so the brand packages match. The header shows the organisation logo, a divider and the wordmark at the prototype's sizes; the OSS build has a text wordmark and no organisation logo. The header buttons are pinned to ml-trainer's 48px/24px because this app's dense preset would shrink them; the menus take a css override for that. Project cards no longer list file names, which almost always read main.py. The names are still loaded for the projects page search. * Reword the home page banner in microbit.org's terms "Python for the BBC micro:bit" and "Write a program, try it in the simulator, then send it to your micro:bit." replace the flash and MicroPython wording, which is ours rather than a student's. * Make the projects page's back-to-home button a white pill with ml-trainer's arrow The ghost variant did not stand out on the brand colour. BackArrow is copied from ml-trainer unchanged rather than promoted to @microbit/ui. * Use the plain button variant for the page header's settings and help menus The menus' sidebar variant, made for the editor's black chrome, brought its white-pill hover to the purple header. The family shows no hover state on icon buttons in the brand-coloured bar, so the pages pass the base recipe's plain variant; the editor keeps sidebar. * Update the home spec for the reworded banner heading * Scope the editor's drop target to the editor, give the projects page one, and confirm before replacing files The editor's drop target had moved from around the editor to around the router when the pages arrived, so a drop on a page took the editor path. On the projects page in a tab with no project open yet that write waited silently on the storage promise and landed in whatever project was opened next; on the home page the nested page target took the drop and the outer overlay, never seeing a drag-leave, stayed over the editor. The target now wraps the editor routes only, and the projects page has its own with the home page's semantics: the files make a new project that opens in the editor. Without the projects database a hex or idea asks before replacing an edited project again, with the strings from main so translations survive; the before-unload prompt only guards closing the tab, not an in-tab replace. Adding files that would overwrite existing ones asks first in every mode. load-error-mixed no longer claims a hex replaces the project. * Show the beta notice as a band under the page header and drop the sidebar notice's More button The pages had no notice; they now show ml-trainer's band with its longer copy and a Feedback button, under the header. The editor keeps its short sidebar notice. Both are English only, as they show on non-public stages only. The welcome video is reached from the welcome dialog alone. * Take the project idea card images from the website's make pages The three animated gifs give way to the site's stills, so the imports and the OSS placeholders change extension. * Use ml-trainer's minimal placeholders for the OSS image fallbacks A 4x3 grey rectangle for cards and an empty SVG for the banner background, as ml-trainer has, in place of drawn illustrations. * WIP notes on the home page/multiple projects work. * Migrate to the reviewed ui-patterns project component API - ProjectSearchInput, ProjectSortInput and ProjectSortDirection. - useProjectActions takes selectedIds as an array; its delete action is `delete`. useProjectPageActions passes the array through. - ProjectCard's onToggleSelected. - Skip-to-toolbar uses ProjectsToolbar's focus handle instead of a wrapper ref and DOM query. - sortProjects collates in the app locale. * Add ml-trainer's footer to the home page Copyright, cookies, privacy and terms; the app store badges are left out. The copyright holder comes from the brand package, as in ml-trainer. * Pin the released ui packages and the theme branch build @microbit/ui 0.5.0, ui-carousel 0.4.0 and ui-patterns 0.7.0 replace the linked ../ui checkout. The theme version in build.yml is a temporary build from the theme package's projects-pages branch. * Fix #project: links on a deployment under a base URL Two bugs, both only visible where the app isn't served from the root, so neither shows in local development and both are live on the review build. The editor's loader redirected to `request.url`'s pathname to drop the hash, but that carries the basename and `redirect` adds it again, landing on /v/3/v/3/project. It now strips the basename first. The home page loader only took a hash starting with #project:, while microbit.org links put it behind the v2 editor's #import: prefix. Such a link reached the home page with its program silently dropped. * Match page URLs against the deployment's base URL in the e2e tests The assertions took the app to be at the root, so on the review and staging deployments the home page's URL, which is the base URL without its trailing slash, matched nothing. appUrlPattern builds the expected URL from E2E_BASE_URL, as appUrl already does, and is stricter than the patterns it replaces: /projects no longer satisfies an assertion for /project. * Set the e2e cookies on the context, not the app fixture Tests that drive only the home or projects page never took the app fixture, so on Foundation builds they met the compliance notice. It is a modal dialog, which hides the page from the accessibility tree, and the headings they wait for did not exist. * Upload the Playwright report CI actually writes The html reporter writes playwright-report/, so the upload step found nothing and failing runs left no screenshots or traces behind. * Serve the base URL without its trailing slash from vite preview react-router renders the route at "/" as the bare basename, so the app's own home page link is /projects-pages, not /projects-pages/. The deployments serve that; vite preview, which the e2e tests run against, returned its own 404 page, so reloading the home page failed. * Give the OSS build a logo to click The sidebar logo links home now that there is a home page, but the default brand config left squareLogo and horizontalLogo unset. The boxes that hold them take their height from their content, so an OSS build had a zero-height link and no way to reach the home page. It also stopped the e2e tests running against a build without the brand package. * Size the drop targets to their content so the home footer sits at the bottom FileDropTarget fixed its height at 100% of its parent, which suited the editor's full-height pane but sized the home page's target to one viewport regardless of the content inside it. The rows overflowed the target and the new footer was laid out at the one-viewport mark, roughly half way down the page, with the drag overlay only dimming the first screen. The target now takes its layout from the caller: the editor keeps the full height, the home and projects pages grow with their content. On tall windows the footer is pushed to the bottom of the window.
This will make it easier to discuss when it should show.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is our integration branch.
There's still quite a bit to resolve before this is ready to merge back, along with coordination issues around documentation, videos, PD and similar.