From 51145b0ecabf55f54eb7188cf16e4559bd8f09ff Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Mon, 17 Nov 2025 16:44:06 -0500 Subject: [PATCH 1/8] WIP: one-step setup wizard --- app/assets/stylesheets/wizard.scss | 254 +---------------- app/serializers/wizard_step_serializer.rb | 27 +- config/locales/client.en.yml | 81 +----- config/locales/server.en.yml | 23 +- .../static/wizard/components/wizard-step.gjs | 263 ++---------------- .../app/static/wizard/models/wizard.js | 12 +- lib/wizard/builder.rb | 81 ++---- lib/wizard/step.rb | 2 +- spec/lib/wizard/step_updater_spec.rb | 57 ++-- spec/lib/wizard/wizard_builder_spec.rb | 40 +-- spec/serializers/wizard_serializer_spec.rb | 14 +- spec/system/wizard_spec.rb | 35 +-- 12 files changed, 113 insertions(+), 776 deletions(-) diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss index c40d1e159493b..ee610553bd20d 100644 --- a/app/assets/stylesheets/wizard.scss +++ b/app/assets/stylesheets/wizard.scss @@ -3,20 +3,6 @@ $blob-bg: absolute-image-url("/branded-background.svg"); $blob-mobile-bg: absolute-image-url("/branded-background-mobile.svg"); -@keyframes bump { - 0% { - transform: scale(1); - } - - 50% { - transform: scale(1.05); - } - - 100% { - transform: scale(1); - } -} - // horizon specific style .experimental-screen { .wizard & { @@ -59,7 +45,7 @@ body.wizard { .discourse-logo svg { position: relative; - height: 70px; + height: 50px; width: auto; @include viewport.until(sm) { @@ -72,12 +58,6 @@ body.wizard { } } } - - .wizard-canvas { - position: fixed; - top: 0; - pointer-events: none; - } } // Refactored SCSS @@ -118,40 +98,10 @@ body.wizard { width: 100%; } - &__step-counter { - text-align: center; - font-weight: 700; - color: var(--primary-high); - text-shadow: 1px 1px 12px var(--secondary); - } - - &__step-description { - font-size: var(--font-up-2); - flex: 1 0 40%; - - @include viewport.until(md) { - font-size: var(--font-up-2); - } - } - &__field { margin-bottom: 1em; } - &__field.checkbox-field { - margin-bottom: 1.5em; - } - - &__field.invalid input { - outline: 0; - border: 3px solid var(--danger); - - @media (prefers-reduced-motion: no-preference) { - animation: bump 0.25s ease-in-out; - animation-iteration-count: 2; - } - } - &__field label { display: block; } @@ -165,43 +115,19 @@ body.wizard { } } - &__sidebar { - width: 230px; - box-sizing: border-box; - margin-right: 1em; - - @media only screen and (width <= 925px) { - width: 100%; - margin-left: auto; - margin-right: auto; - } - - + .wizard-container__fields { - padding: 1em; - background: var(--primary-very-low); - width: auto; - border-radius: 0.5em; - margin-top: -1em; - - @media only screen and (width <= 925px) { - display: none; - } - } - } - &__step-header { text-align: center; margin-bottom: 3em; &--emoji img { - width: 30px; - height: 30px; + width: 40px; + height: 40px; margin-bottom: 1em; } } &__step-title { - font-size: 2.75em; + font-size: 2.5em; color: var(--primary); line-height: var(--line-height-medium); margin: 0 0 0.5em 0; @@ -227,28 +153,6 @@ body.wizard { } } - &__buttons-left { - display: flex; - flex-wrap: wrap; - gap: 1em; - align-items: center; - - @include viewport.until(sm) { - order: 2; - } - } - - &__buttons-right { - display: flex; - align-items: center; - font-weight: bold; - - @include viewport.until(sm) { - margin-right: 0; - flex-direction: column; - } - } - &__step-text { display: inline; @@ -300,71 +204,9 @@ body.wizard { background-color: var(--primary-medium); } - &__button.finish { - @include viewport.until(sm) { - order: 2; - } - } - - &__button.next { - min-width: 70px; - margin-left: 1em; - - @include viewport.until(sm) { - order: 1; - margin-left: 0; - } - } - - &__button.danger { - background-color: var(--danger); - color: var(--secondary); - } - - &__button.danger:hover, - &__button.danger:focus { - background-color: var(--danger-hover); - } - - &__button.danger:active { - background-color: var(--danger-medium); - } - - &__button.danger:disabled { - background-color: var(--danger-low); - } - - &__button-upload { - display: block; - background-color: transparent; - margin-top: 1em; - border: 1px solid var(--tertiary-high); - text-align: center; - color: var(--tertiary-high); - } - - &__button-upload:hover { - background-color: transparent; - border-color: var(--tertiary-hover); - color: var(--tertiary-hover); - } - - &__button-upload svg { - margin-left: 0.5em; - } - - .wizard-hidden-upload-field { - visibility: hidden; - position: absolute; - } - - &__button:last-child { - margin-right: 0; - } - &__step-footer { display: flex; - justify-content: space-between; + justify-content: space-around; align-items: center; @include viewport.until(sm) { @@ -428,92 +270,6 @@ body.wizard { border-radius: 4px; } - &__field.checkbox-field .wizard-container__label { - cursor: pointer; - display: inline-block; - } - - &__checkbox-slider { - display: inline-block; - background: var(--primary-low-mid); - border-radius: 16px; - width: 50px; - height: 28px; - margin-right: 0.5em; - position: relative; - vertical-align: middle; - transition: background 0.25s; - - @media only screen and (width <= 568px) { - height: 20px; - width: 35px; - } - } - - &__checkbox-slider::before, - &__checkbox-slider::after { - content: ""; - display: block; - position: absolute; - } - - &__checkbox-slider::after { - content: "\2713"; // checkmark - color: var(--secondary); - top: 4px; - left: 9px; - - @media only screen and (width <= 568px) { - top: 3px; - left: 5px; - font-size: var(--font-down-3); - } - } - - &__checkbox-slider::before { - background: var(--secondary); - border-radius: 50%; - width: 20px; - height: 20px; - top: 4px; - left: 4px; - transition: left 0.25s; - - @media only screen and (width <= 568px) { - height: 12px; - width: 12px; - } - } - - &__field.checkbox-field - .wizard-container__label:hover - .wizard-container__checkbox-slider::before { - background: var(--secondary); - box-shadow: 0 0 0 1px rgb(0, 0, 0, 0.15); - } - - &__checkbox:checked + .wizard-container__checkbox-slider { - background: var(--tertiary); - } - - &__checkbox:checked + .wizard-container__checkbox-slider::before { - left: 26px; - - @media only screen and (width <= 568px) { - left: 20px; - } - } - - &__checkbox { - position: absolute; - visibility: hidden; - } - - &__checkbox-label { - position: relative; - top: 2px; - } - &__radio { position: absolute; visibility: hidden; diff --git a/app/serializers/wizard_step_serializer.rb b/app/serializers/wizard_step_serializer.rb index 1fe6a15cafab6..78a53bbb7ecfb 100644 --- a/app/serializers/wizard_step_serializer.rb +++ b/app/serializers/wizard_step_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class WizardStepSerializer < ApplicationSerializer - attributes :id, :next, :previous, :description, :title, :index, :emoji + attributes :id, :title, :index, :emoji has_many :fields, serializer: WizardFieldSerializer, embed: :objects def id @@ -12,22 +12,6 @@ def index object.index end - def next - object.next.id if object.next.present? - end - - def include_next? - object.next.present? - end - - def previous - object.previous.id if object.previous.present? - end - - def include_previous? - object.previous.present? - end - def i18n_key @i18n_key ||= "wizard.step.#{object.id}".underscore end @@ -39,15 +23,6 @@ def translate(sub_key, vars = nil) vars.nil? ? I18n.t(key) : I18n.t(key, vars) end - def description - key = object.disabled ? "disabled" : "description" - translate(key, object.description_vars) - end - - def include_description? - description.present? - end - def title translate("title") end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f9e96ed295ab2..8482e83004b26 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -8585,83 +8585,4 @@ en: wizard_js: wizard: - jump_in: "Jump in!" - finish: "Exit setup" - back: "Back" - next: "Next" - configure_more: "Configure more…" - step-text: "Step" - step: "%{current} of %{total}" - upload: "Upload file" - uploading: "Uploading…" - upload_error: "Sorry, there was an error uploading that file. Please try again." - - staff_count: - one: "Your community has %{count} staff (you)." - other: "Your community has %{count} staff, including you." - - invites: - add_user: "add" - none_added: "You haven’t invited any staff. Are you sure you want to continue?" - roles: - admin: "Admin" - moderator: "Moderator" - regular: "Regular User" - - homepage_choices: - custom: - label: "Custom" - description: "Display a %{type}-focused homepage with users landing on %{landingPage}" - style_type: - categories: "category" - topics: "topic" - - top_menu_items: - new: "New" - unread: "Unread" - top: "Top" - latest: "Latest" - hot: "Hot" - categories: "Categories" - unseen: "Unseen" - read: "Read" - bookmarks: "Bookmarks" - - previews: - topic_title: "What books are you reading?" - share_button: "Share" - reply_button: "Reply" - topic_preview: "Topic preview" - homepage_preview: "Homepage preview" - - homepage_preview: - nav_buttons: - all_categories: "all categories" - topic_titles: - what_books: "What books are you reading?" - what_movies: "What movies have you seen recently?" - random_fact: "Random fact of the day" - tv_show: "Recommend a TV show" - what_hobbies: "What are your hobbies?" - what_music: "What are you listening to right now?" - funniest_thing: "Funniest thing you've seen today" - share_art: "Share your art!" - topic_ops: - what_books: | - We all love to read, let's use this topic to share our - current or recent reads. I'm a fantasy fan and I've been - re-reading The Lord of the Rings for the 100th time. - What about you? - category_descriptions: - icebreakers: "Get to know your fellow community members with fun questions." - news: "Discuss the latest news and events." - site_feedback: "Share your thoughts on the community and suggest improvements." - category_names: - icebreakers: "Icebreakers" - news: "News" - site_feedback: "Site Feedback" - table_headers: - topic: "Topic" - replies: "Replies" - views: "Views" - activity: "Activity" + jump_in: "Let's go!" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 37afcf179f59a..37f784b7b2b51 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -5526,24 +5526,16 @@ en: wizard: title: "Discourse Setup" step: - introduction: - title: "About your site" - description: "These will be shown on your login and any public pages. You can always change them later." + setup: + title: "Getting started" + description: "Let's get your community set up with the basics. You can always change these later." fields: title: label: "Community name" - placeholder: "Jane’s Hangout" - site_description: - label: "Describe your community in a sentence" - placeholder: "A place for Jane and her friends to discuss cool stuff" + placeholder: "Jane's Hangout" default_locale: label: "Language" - - privacy: - title: "Member access" - - fields: login_required: label: "Visibility" description: "Is your community public or private?" @@ -5567,13 +5559,6 @@ en: label: "No, new members can join immediately" "yes": label: "Yes, new members must be approved by moderators" - chat_enabled: - placeholder: "Enable chat" - extra_description: "Engage with your members in real time" - - ready: - title: "Your site is ready!" - description: "That's it! You've done the basics to setup your community. Now you can jump in and have a look around, write a welcome topic, and send invites!

Have fun!" styling: title: "Look and feel" diff --git a/frontend/discourse/app/static/wizard/components/wizard-step.gjs b/frontend/discourse/app/static/wizard/components/wizard-step.gjs index 3f88d635c814f..38a89f21c86cb 100644 --- a/frontend/discourse/app/static/wizard/components/wizard-step.gjs +++ b/frontend/discourse/app/static/wizard/components/wizard-step.gjs @@ -1,21 +1,12 @@ import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; import { on } from "@ember/modifier"; import { action } from "@ember/object"; -import didInsert from "@ember/render-modifiers/modifiers/did-insert"; -import didUpdate from "@ember/render-modifiers/modifiers/did-update"; -import { schedule } from "@ember/runloop"; -import { htmlSafe } from "@ember/template"; import concatClass from "discourse/helpers/concat-class"; import emoji from "discourse/helpers/emoji"; import { i18n } from "discourse-i18n"; import WizardField from "./wizard-field"; -const READY_STEP_INDEX = 3; - export default class WizardStepComponent extends Component { - @tracked saving = false; - get wizard() { return this.args.wizard; } @@ -28,51 +19,6 @@ export default class WizardStepComponent extends Component { return this.step.id; } - // We don't want to show the step counter for optional steps after - // the "Ready" step. - get showStepCounter() { - return this.args.step.displayIndex < READY_STEP_INDEX; - } - - /** - * Step Back Button? Primary Action Secondary Action - * ------------------------------------------------------------------ - * First No Next N/A - * ------------------------------------------------------------------ - * ... Yes Next N/A - * ------------------------------------------------------------------ - * Ready Yes Jump In Configure More - * ------------------------------------------------------------------ - * ... Yes Next Exit Setup - * ------------------------------------------------------------------ - * Last Yes Jump In N/A - * ------------------------------------------------------------------ - * - * Back Button: without saving, go back to the last page - * Next Button: save, and if successful, go to the next page - * Configure More: re-skinned next button - * Exit Setup: without saving, go to the home page ("finish") - * Jump In: on the "ready" page, it exits the setup ("finish"), on the - * last page, it saves, and if successful, go to the home page - */ - get isFinalStep() { - return this.step.displayIndex === this.wizard.steps.length; - } - - get showBackButton() { - return this.step.index > 0; - } - - get showFinishButton() { - const ready = this.wizard.findStep("ready"); - const isReady = ready && this.step.index > ready.index; - return isReady && !this.isFinalStep; - } - - get showJumpInButton() { - return this.id === "ready" || this.isFinalStep; - } - get includeSidebar() { return !!this.step.fields.find((f) => f.showInSidebar); } @@ -92,163 +38,32 @@ export default class WizardStepComponent extends Component { } @action - stepChanged() { - this.saving = false; - this.autoFocus(); - } - - @action - onKeyUp(event) { - if (event.key === "Enter") { - if (this.showJumpInButton) { - this.jumpIn(); - } else { - this.nextStep(); - } - } - } - - @action - autoFocus() { - schedule("afterRender", () => { - const firstInvalidElement = document.querySelector( - ".wizard-container__input.invalid:nth-of-type(1) .wizard-focusable" - ); - - if (firstInvalidElement) { - return firstInvalidElement.focus(); - } - - document.querySelector(".wizard-focusable:nth-of-type(1)")?.focus(); - }); - } - - async advance() { - try { - this.saving = true; - const response = await this.step.save(); - this.args.goNext(response); - } finally { - this.saving = false; - } - } - - @action - finish(event) { - event?.preventDefault(); - - if (this.saving) { - return; - } - + async jumpIn() { + await this.step.save(); this.args.goHome(); } - @action - jumpIn(event) { - event?.preventDefault(); - - if (this.saving) { - return; - } - - if (this.id === "ready") { - this.finish(); - } else { - this.nextStep(); - } - } - - @action - backStep(event) { - event?.preventDefault(); - - if (this.saving) { - return; - } - - this.args.goBack(); - } - - @action - nextStep(event) { - event?.preventDefault(); - - if (this.saving) { - return; - } - - if (this.step.validate()) { - this.advance(); - } else { - this.autoFocus(); - } - } -