feat(dashboard): store admin SPA with the product catalog flow - #26
Merged
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… layout Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tions table Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… inventory table Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d a flat inventory table" This reverts commit e6c733d.
Homepage-complete second theme for the shop_themes engine, ported from the Pixio bootstrap template. Splits the shared document shell out of the default theme into shop_base_theme/components/base.html so a theme owns only its chrome. Vendored CSS, fonts, icons and demo photography come from the public Pixio demo and are NOT licensed - they must be replaced from the purchased package and the demo imagery stripped before this ships anywhere public. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The homepage sections all read Pixio Theme Settings, which until now was only populated by throwaway console scripts - so any fresh site rendered every section empty. Idempotent: tables fill only while empty, scalars only while falsy, so a re-run never clobbers merchant-curated content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Words come from the enabled category tree rather than a settings field, so the strip self-updates instead of drifting out of sync with the menu. Two tracks instead of the reference's one: upstream pins its single ul to the wrapper width while the content is 2.7x wider, so its -100% keyframe scrolls one viewport and snaps mid-word. Duplicating the track makes the loop seamless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(core): read the customer group from Selling Settings _create_party_for_user read a customer_group field off Lifestyle Settings that has never existed, so party creation threw and every checkout failed at the quotation step - on the un-themed storefront too, not just Pixio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(core): resolve a leaf customer group for party creation
get_root_of("Customer Group") returns the is_group=1 root, which Customer
rejects, so an unset Selling Setting still broke checkout. Falls through to
frappe's default then to any leaf group.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ettings tabs Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ERPNext moved its transaction mappers into sibling `mapper` modules, and both layouts are live across the versions this app runs against. The pinned import raised at module import time, which took `ls_shop.migrate` down with it and skipped the whole `after_migrate` hook - so route seeding, the search index and the payment modes never ran on an older bench. Mirrors the resolution already used in `api/admin/orders.py`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
XS-XXL are labels, not measurements, so `numeric_values` made ERPNext demand a from/to range on Item Attribute and throw "From Range has to be less than To Range". It is the first step of the installer, so the whole demo catalog failed before anything was created. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`configure_lifestyle_settings` never set the mandatory company, and steps 5 and 6 assumed the Website Item doctype exists. Website Item ships with webshop, which this app no longer depends on, so on a bench without it the installer threw and rolled the whole catalog back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Builds the menu the storefront header renders, without opening Desk. The tree is drag and drop at every level: dropping inside another entry re-parents it, dropping between siblings reorders them, and sortable's own move hook refuses a drop that would bury a branch past the depth the server allows, so the gesture never animates into an error. An entry can link to several item groups at once, which is the case Desk's Link field could not express - products from every listed group are shown together under the one entry. Building the whole menu from an existing item group tree is one action. The preview frames the real storefront rather than mocking a menu, so what is checked is the theme's own header. Every mutation returns the whole tree, so the editor adopts the server's answer instead of patching a local copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test sampled five arbitrary Sales Orders and compared the cost of reading the first against the cost of reading all five. An order with no paperwork skips the delivery-note and packing-slip reads entirely, so as soon as the newest order on a site had no delivery note the two numbers differed for a reason that has nothing to do with scaling, and the test failed on data rather than on code. Measured directly, the reader costs four queries for one order and four for five. Sampling only orders that carry a delivery note keeps the assertion about what it claims to be about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdA6pnQb2yRMg8RFZdV1pH
An audit found 22 controls and screens that look finished and are not. They do not crash, so clicking through never caught them. Each one is now wired to a real endpoint, or gone. Wired: delete a product (refusing, with a reason, anything that has ever been ordered), copy and preview the storefront link, receive stock, set the low-stock level, move products into a collection from both the bulk bar and the product menu, print a delivery note and an invoice, publish the products under a menu entry, view the storefront, log out, and refund an order. Settings now reads and writes the store's own records instead of rendering an invented company, an invented warehouse and three invented members of staff; its long tail of fields comes from the doctype's own meta, so adding a field surfaces it. The import wizard's Images step was the worst of them: a setTimeout progress bar ending in "356 of 372 images matched by file name", hardcoded, over an import that really did create the products. Photos are now really uploaded, really matched to a product and colour by file name, and really attached - and every number on the screen is counted from the files chosen. Removed rather than dressed up: product types (a routed screen with no backend at all, leaking mock rows onto the real product form), a simulated zip import, a storefront listing form that saved nowhere, the URL-column photo mode (making it real means fetching arbitrary URLs server-side), and the grey furniture whose only purpose was to name a feature that does not exist. Two screens turned out to have a wired implementation sitting unused beside a mock: the theme editor and the refund dialog were both complete and imported by nothing, while the route and the menu pointed at fixtures. Both now reach the real one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdA6pnQb2yRMg8RFZdV1pH
Saving the cart Quotation resolves the party's receivable account, and ERPNext's
account_perm_check calls frappe.has_permission("Account") directly, so no
ignore_permissions reaches it and no storefront role has Account read. Every
write to the cart in the shopper's own session was refused: Place Order, the
checkout page's shipping rule, the address step, the coupon, the delivery
charges. Nobody could buy anything.
system_user_session() already existed three functions away, its docstring already
named this exact ERPNext behaviour, and place_order and place_cod_order already
used it - the cart saves did not. They do now, through one helper, with only the
save inside the elevated window: the caller has already proved the cart belongs
to the session user, because _get_cart_quotation scopes on contact_email, and
every ownership and validation check still runs as the shopper.
The stock check existed too, and only the cart's own JavaScript ever called it,
so a tampered client could order 9999 of an item with 15 in stock and get a
quotation for it. It is enforced on the server now, through the same function
rather than a second implementation.
An item with no row in the sale price list showed a real price on the product
page and nothing in the cart, because the cart alone had no fallback. It falls
back the same way the product page does. Note this fixes what the cart shows,
not the rate ERPNext books: the quotation still sells from the sale price list
alone, which is a decision about how the two configured lists relate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdA6pnQb2yRMg8RFZdV1pH
Storefront Pages had no way in at all: both "Add page" buttons passed `to`, which is not a frappe-ui Button prop, so it landed in $attrs as an inert DOM attribute. With no pages on the site there was no row to click either, and a working editor sat behind a button that did nothing. It is the only misuse of that prop in the tree. The theme editor's preview pane was a 500. theme_editor_preview imported two names that had never existed, so the module failed on import - invisible until the Theme route stopped pointing at a mock and started rendering the real editor. render_page_preview now renders the live theme's home page the way a real request does, and blanks the tracking blocks so an editor pane emits no analytics. Saving a price of zero reported "Price updated" and wrote nothing: the pricing helper treats a non-positive rate as "leave this list alone", which the create flow depends on, so the refusal belongs at the endpoint. It says a price cannot be removed once set, because there is nowhere in the app that can remove one. The Overview's attention tiles counted the preview list, which the API caps at five, so they read five however many there were. Both totals come from the server now, out of the same pass that builds the preview. Also: bulk actions no longer sit enabled over an empty selection; three menu rows that promised editing now say what they really do - a read-only list of addresses, a read-only list of options, and the unfiltered orders list; Settings no longer states that no company is set while it is still asking; an unknown /commera path gets a real not-found screen instead of an empty shell; the variant dialog stops rendering the literal word undefined; and a Margin column that could never hold a value is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdA6pnQb2yRMg8RFZdV1pH
Orders offered Export and Create order, Customers offered Export and Add customer, and all four only ever toasted that they were not supported. Create order and Add customer contradict a decision already made: a shopper places their own order and a customer record is created the moment they check out, which is why validate_document_access compares owner to the session user. The two exports have no endpoint, the same reason the product CSV export went. The import wizard keeps its six sources. Only the spreadsheet works, but the other five stay named and badged: a merchant arriving from Shopify should see that we know about it and that it is not their way in yet. That is what the step's own comment argued, and it is a different case from a button that looks like every other working button in a header. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdA6pnQb2yRMg8RFZdV1pH
Publishing one option and receiving stock against it both existed, on the variant's own page - product, swatch, dialog, "Open full page". Three clicks from the matrix that shows every option, which is where a merchant is standing when they want either. Both are row actions on that matrix now, and the matrix says what the storefront thinks: Live, Hidden, or the reason it cannot go live yet. That reason is the server's own blocker list, the same text set_variant_published refuses with, so an option missing a photo says so before the click rather than after it. Receiving is the additive receipt the Stock screen and the product menu already use - one quantity onto every size of the option - because ls_shop has no way to set on-hand to an exact number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdA6pnQb2yRMg8RFZdV1pH
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127esXD5REhi7otqsBLYVaW
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127esXD5REhi7otqsBLYVaW
…mark Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
The variant matrix edited the compare-at while the storefront charges the sale rate, so a price rise never reached the shopper. Settings autosave shared one request across every field, so overlapping commits resolved against each other. A failed import left `finished` set, so every retry repainted a success that never ran. The shell reached the server on the v1 path the api wrappers exist to avoid. The login selector in the e2e harness went stale against Frappe's login page; the suite could not reach the app at all without it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
The global file's frontend doctrine is written for the storefront's Jinja and Alpine pages and says no SPA frameworks, so every reviewer of this directory was applying the wrong law to it. Nine numbered sections a review can cite: the one door to the server, who toasts a failure, how a settings panel saves itself, where a file goes, what to reuse before building, tokens, comments, style, and how to verify. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
…rules The appearance cards marked the chosen one with three more pixels of border, so nothing read as selected. They are ours now rather than frappe-ui's deprecated switcher: a tick on the chosen card, native radios for the keyboard, and the two CSS overrides that reached into the old component's internals are gone. A rule sat between a group's heading and the first field under it, fencing the heading off from what it heads. The delete action no longer announces itself under a Danger heading, and the Advanced warning has room above it. The comments that cited the wiring map now carry the fact itself, since that file goes in the next commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Planning and audit notes from building the dashboard, all of them spent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Rl0007
marked this pull request as ready for review
September 8, 2026 05:50
…hips frappe-ui's preset defines gray, blue, green, red, orange, yellow, teal, violet, cyan, amber, pink and purple — and no neutral. Every bg-neutral-* in the previews emitted no rule at all, so the desktop backdrop, the dark window and all three content lines in every frame painted transparent: in the light theme the three cards were identical white rectangles, and in the dark theme the dark card was empty but for its traffic lights. The cards also stack below sm, where three frames across a phone read as portrait slivers rather than windows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Four of the five failing jobs were failing on their own setup rather than on the code. Server never installed bwh_payments or bwh_shipping, which the app imports 34 times; bwh_shipping was not in required_apps either. Dashboard Typecheck cached a yarn.lock that is not tracked, installed with yarn where the repo tracks package-lock.json, and ran a typecheck script that does not exist against a codebase with no TypeScript. It builds the dashboard now, which is the check that was actually meant, and the lockfile it needs was out of sync with package.json so npm ci could not have run either. Semantic Commits had no config, so it rejected this repo's own `merge:` subject under commitlint's defaults. Pre-Commit was linting code we do not author: biome retabbed vendored Alpine and every Frappe-written .json — doctypes and record exports that bench rewrites at one-space indent — and the comment cap read third-party licence banners. The findings that were ours are fixed: a forEach, an assignment inside an expression, and the formatting biome could apply itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Dead since the output moved to public/commera, and a wide `git add` swept all 63 files of it into a commit once already. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Whole repo rather than the lines this PR happens to touch: semgrep ci picks by baseline, and the baseline moves with every push, so a line-scoped fix only defers the failure. 119 findings to zero. Two were not lint. enqueue_publish_all_variants inserted its log row in the request transaction and the job read that row immediately, so a fast worker raised DoesNotExistError before the request had committed - it takes enqueue_after_commit now. And api/translation.py served the entire Translation table to guests with no rate limit; nothing imported it, so it is gone. The suppressions that were already here did nothing: semgrep reads `# nosemgrep: <text>` as naming a rule, so a prose reason suppresses a rule by that name, which is to say nothing at all. Every one now names the rule id. The rest is 84 missing argument type hints, each read off the body rather than guessed, and guest endpoints and manual commits kept where they are correct with the reason recorded against them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
The concurrency group keyed on github.event.number, which is empty on push - so every push to develop and main shared one group and cancelled the one before it. The linters pinned Python 3.13 while pyproject requires >=3.14, so semgrep and ruff ran on a version the app does not support. And a Server job that dies before the tests print nothing at all; it dumps bench_start.log and the site logs now, the way every upstream Frappe repo does. Dependabot watches the dashboard lockfile and the actions, weekly, minor and patch grouped. The audit gates on critical rather than high because the tree already carries a high - vite <=6.4.2 through esbuild - that only a major bump clears; a check that lands red teaches people to ignore it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
The hook added in 9905d83 has never passed: 141 comments and docstrings ran past it, so the Pre-Commit job has been red since the day the rule landed. Each one keeps the fact and loses the exposition, per the rule's own test - a quirk, an ordering requirement or a defect reference stays; why it is built this way goes in the commit message. Nothing was deleted wholesale, because every block carried something worth keeping. Three facts that only made sense against one specific line moved to sit on that line instead: the mask-size stretch on the promo band, the breakpoint ladder that keeps .swiper-product apart from the rails, and the shared Item Attribute the onboarding test appends to. Comments only. Every touched Python file parses to the same tree with docstrings blanked, and every touched stylesheet is byte-identical once comments are stripped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Pre-Commit was not reporting lint errors: ruff-format and biome-check are fixing hooks, and they were rewriting six Python files and the commitlint config on every run, which pre-commit reports as a failure. The drift predates this branch - the pinned ruff 0.8.1 wraps six long lines a newer local ruff leaves alone. Formatting only: every file parses to an identical tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Two bugs stacked, and the first hid the second.
get_refund_status looked for Payment Entry Reference rows against the Sales
Order, but the capture chain books the payment on the Sales Invoice
(api/payments.py:213 raises it with get_payment_entry("Sales Invoice", ...)),
so no paid order ever matched and every one reported can_refund: False.
get_order_payments walks the invoice hop, and keeps the direct Sales Order
shape too so a Desk advance still resolves.
Behind that, the Payment Entry could never insert: validate_mandatory() runs
before set_amounts(), so set_received_amount() never derives received_amount
and ERPNext refuses the entry. It is set explicitly now, guarded by a currency
check rather than an assumption - a payment that crossed currencies is refused
and sent to the accounts desk instead of paid out at an invented rate.
This also unblocks the whole suite: the missing get_order_payments broke Frappe's
test discovery, so `run-tests --app ls_shop` collected nothing at all. 505 tests
run now where none did.
ls_shop.tests.test_order_refunds: 9 of 9 pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
The type hints added in 69c9477 read add_node's link_target as a plain string, but set_link runs parse_list over it for the item-group case: linking a menu entry to several groups passes a list, and Frappe enforces the annotation at runtime, so 30 tests raised FrappeTypeError and pydantic's string_type on a path that had worked. Only visible now because the missing get_order_payments had been hiding the whole suite from CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
A bare CI site never runs the setup wizard, so 120 of the 180 failures were one missing Company: Lifestyle Settings' four mandatory fields, the storefront item group, the fiscal year. `before_tests` seeds them the way navgold_custom does, with the two ls_shop traps handled — the wizard needs explicit fiscal-year dates or Company creation silently aborts, and ls_shop makes custom_displayname mandatory on Item Group, so the wizard's own stock groups never insert. Also: migrate in CI, since Shop Theme records sync on migrate and not on install-app; `frappe.db.add_column` now lives in frappe.database.schema; and the suites that indexed [0] into Item / Style Attribute Configurator build their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Second layer under the missing company. `set_defaults_for_tests()` points the customer group and territory at the tree roots, which are group nodes, so every Customer the suite inserts without naming one was refused; and the cart and search suites build their Item Prices from Lifestyle Settings' price lists, which a bare site leaves blank. Also seeds the themed /<lang>/page/<route> entry that Shop Web Page has always needed: it was in no DEFAULT_ROUTES and its patch was in no patches.txt, so a merchant's content page never resolved on a themed storefront. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
… of the run Assigning `frappe.form_dict` rebinds the module attribute and throws away the LocalProxy, so every later test that sets `frappe.local.form_dict` writes somewhere nobody reads and the readers see the stale dict these two left behind. Order dependent, which is why each module passed on its own: the sitemap segment, footer preview, product detail and OG suites only failed in a full run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
…eb Page The Sales Order lock was taken and never given back, so a refused refund locked the order for good — and because the lock is a file rather than a row, it outlived the test rollback and blocked the next order to reuse the freed name. The footer picker still listed core Web Page docs, which render outside the storefront theme; it lists published Shop Web Pages now, as its test always said. Also: two more fixtures reaching for Item[0] on a site that has none, and a ladder test calling a helper only its sibling class defines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
…age picker spec `add_column` leaves `table_columns::` stale, so the migration under test read the table as it was before the ALTER and found nothing to move. Frappe's own db tests clear the key the same way. The footer suite still asserted the picker offers core Web Pages, which the pages suite explicitly forbids — a Web Page renders outside the storefront theme. Same coverage, pointed at Shop Web Page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
Committed rather than attached because GitHub takes PR attachments only through the browser; the description links to this file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbXDZ5bAG5GmPFpDUVswS3
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.
Vue Based Backend, use /commera to access it.
Demo
ci-green-demo.mp4
In the current app you had to jump at least 8 DocTypes just to create an item and add prices to it. Now everything is controlled from a single Vue Frontend.
It has lot of features:
I will write a detailed docs for it.
Apart from this we are also introducing Storefronts like Shopify, now you can update the themes of your customer facing websites with few clicks. You can also create your own Storefronts. I will also add these in docs, maybe will add a tutorial for it.
new-recording-small.mp4