diff --git a/help/demo-organizations.md b/help/demo-organizations.md
index 52751af829..5f7469f44b 100644
--- a/help/demo-organizations.md
+++ b/help/demo-organizations.md
@@ -62,15 +62,35 @@ and set a password for their account.
## Convert a demo organization to a permanent organization
+{!owner-only.md!}
+
+If you'd like to keep your demo organization user and message history,
+you can convert it to a permanent Zulip organization. You'll need to
+choose a new subdomain for your new permanent organization URL.
+
+Also, as part of the process of converting a demo organization to a
+permanent organization:
+
+* Users will be logged out of existing sessions on the web, mobile and
+ desktop apps and need to log in again.
+* Any [API clients](/api) or [integrations](/integrations/) will need
+ to be updated to point to the new organization URL.
+
{start_tabs}
{settings_tab|organization-profile}
-1. Click the **Convert organization** link at the end of the red
- "This is a demo organization" notice on top.
+1. Click the **Convert to make it permanent** link at the end of the
+ "This demo organization will be automatically deleted ..." notice.
-1. Enter the URL you would like to use for the organization and click
- **Convert**.
+1. Enter the subdomain you would like to use for the new organization
+ URL and click **Convert**.
+
+!!! warn ""
+
+ **Note:** You will be logged out when the demo organization is
+ successfully converted to a permanent Zulip organization and be
+ redirected to new organization URL log-in page.
{end_tabs}
diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py
index 90679140e1..13a24f22f1 100644
--- a/tools/lib/capitalization.py
+++ b/tools/lib/capitalization.py
@@ -69,6 +69,7 @@ IGNORED_PHRASES = [
r"keyword",
r"streamname",
r"user@example\.com",
+ r"acme",
# Fragments of larger strings
r"your subscriptions on your Streams page",
r"Add global time
Everyone sees global times in their own time zone\.",
diff --git a/tools/test-js-with-node b/tools/test-js-with-node
index 02894f8464..74477484bb 100755
--- a/tools/test-js-with-node
+++ b/tools/test-js-with-node
@@ -82,6 +82,7 @@ EXEMPT_FILES = make_set(
"web/src/custom_profile_fields_ui.js",
"web/src/dark_theme.ts",
"web/src/debug.ts",
+ "web/src/demo_organizations_ui.js",
"web/src/deprecated_feature_notice.ts",
"web/src/desktop_integration.js",
"web/src/dialog_widget.ts",
diff --git a/web/src/admin.js b/web/src/admin.js
index ea57b86cb9..786119ac6d 100644
--- a/web/src/admin.js
+++ b/web/src/admin.js
@@ -4,6 +4,7 @@ import render_admin_tab from "../templates/settings/admin_tab.hbs";
import render_settings_organization_settings_tip from "../templates/settings/organization_settings_tip.hbs";
import * as bot_data from "./bot_data";
+import * as demo_organizations_ui from "./demo_organizations_ui";
import {$t, get_language_name, language_list} from "./i18n";
import {page_params} from "./page_params";
import {realm_user_settings_defaults} from "./realm_user_settings_defaults";
@@ -232,6 +233,11 @@ export function build_page() {
settings_invites.update_invite_user_panel();
insert_tip_box();
+ if (page_params.demo_organization_scheduled_deletion_date && page_params.is_admin) {
+ demo_organizations_ui.insert_demo_organization_warning();
+ demo_organizations_ui.handle_demo_organization_conversion();
+ }
+
$("#id_realm_bot_creation_policy").val(page_params.realm_bot_creation_policy);
$("#id_realm_digest_weekday").val(options.realm_digest_weekday);
diff --git a/web/src/demo_organizations_ui.js b/web/src/demo_organizations_ui.js
new file mode 100644
index 0000000000..aa7537fcb5
--- /dev/null
+++ b/web/src/demo_organizations_ui.js
@@ -0,0 +1,107 @@
+import $ from "jquery";
+
+import render_convert_demo_organization_form from "../templates/settings/convert_demo_organization_form.hbs";
+import render_demo_organization_warning from "../templates/settings/demo_organization_warning.hbs";
+
+import * as channel from "./channel";
+import * as dialog_widget from "./dialog_widget";
+import {$t} from "./i18n";
+import * as keydown_util from "./keydown_util";
+import {get_demo_organization_deadline_days_remaining} from "./navbar_alerts";
+import {page_params} from "./page_params";
+import * as settings_config from "./settings_config";
+import * as settings_data from "./settings_data";
+import * as settings_org from "./settings_org";
+
+export function insert_demo_organization_warning() {
+ const days_remaining = get_demo_organization_deadline_days_remaining();
+ const rendered_demo_organization_warning = render_demo_organization_warning({
+ is_demo_organization: page_params.demo_organization_scheduled_deletion_date,
+ is_owner: page_params.is_owner,
+ days_remaining,
+ });
+ $(".organization-box").find(".settings-section").prepend(rendered_demo_organization_warning);
+}
+
+export function handle_demo_organization_conversion() {
+ $(".convert-demo-organization-button").on("click", () => {
+ if (!page_params.is_owner) {
+ return;
+ }
+
+ const email_set = !settings_data.user_email_not_configured();
+ const parts = new URL(page_params.realm_uri).hostname.split(".");
+ parts.shift();
+ const domain = parts.join(".");
+ const html_body = render_convert_demo_organization_form({
+ realm_domain: domain,
+ user_has_email_set: email_set,
+ realm_org_type_values: settings_org.get_org_type_dropdown_options(),
+ });
+
+ function demo_organization_conversion_post_render() {
+ const $convert_submit_button = $(
+ "#demo-organization-conversion-modal .dialog_submit_button",
+ );
+ $convert_submit_button.prop("disabled", true);
+ $("#add_organization_type").val(page_params.realm_org_type);
+
+ if (!email_set) {
+ // Disable form fields if demo organization owner email not set.
+ $("#add_organization_type").prop("disabled", true);
+ $("#new_subdomain").prop("disabled", true);
+ } else {
+ // Disable submit button if either form field blank.
+ $("#convert-demo-organization-form").on("input change", () => {
+ const string_id = $("#new_subdomain").val().trim();
+ const org_type = $("#add_organization_type").val();
+ $convert_submit_button.prop(
+ "disabled",
+ string_id === "" ||
+ Number.parseInt(org_type, 10) ===
+ settings_config.all_org_type_values.unspecified.code,
+ );
+ });
+ }
+ }
+
+ function submit_subdomain() {
+ const $string_id = $("#new_subdomain");
+ const $organization_type = $("#add_organization_type");
+ const data = {
+ string_id: $string_id.val(),
+ org_type: $organization_type.val(),
+ };
+ const opts = {
+ success_continuation(data) {
+ window.location.href = data.realm_uri;
+ },
+ };
+ dialog_widget.submit_api_request(channel.patch, "/json/realm", data, opts);
+ }
+
+ dialog_widget.launch({
+ html_heading: $t({defaultMessage: "Make organization permanent"}),
+ html_body,
+ on_click: submit_subdomain,
+ post_render: demo_organization_conversion_post_render,
+ html_submit_button: $t({defaultMessage: "Convert"}),
+ id: "demo-organization-conversion-modal",
+ loading_spinner: true,
+ help_link:
+ "/help/demo-organizations#convert-a-demo-organization-to-a-permanent-organization",
+ });
+ });
+
+ // Treat Enter with convert demo organization link as a click.
+ $(".demo-organization-warning").on(
+ "keyup",
+ ".convert-demo-organization-button[role=button]",
+ function (e) {
+ e.stopPropagation();
+ if (keydown_util.is_enter_event(e)) {
+ $(this).trigger("click");
+ }
+ },
+ );
+}
diff --git a/web/styles/app_components.css b/web/styles/app_components.css
index f61b813f22..e155099465 100644
--- a/web/styles/app_components.css
+++ b/web/styles/app_components.css
@@ -587,6 +587,30 @@ div.overlay {
margin-right: 8px;
}
+.demo-organization-warning {
+ position: relative;
+ display: block;
+ background-color: hsl(4deg 35% 90%);
+ border: 1px solid hsl(3deg 57% 33% / 40%);
+ border-radius: 4px;
+ padding: 10px;
+ margin: 10px 0;
+ font-size: 1rem;
+ line-height: 1.5;
+ color: hsl(4deg 58% 33%);
+
+ a {
+ text-decoration: none;
+ }
+
+ .convert-demo-organization-button {
+ &:focus {
+ outline: 1px solid hsl(200deg 100% 25%);
+ outline-offset: 0;
+ }
+ }
+}
+
/* We are mostly consistent in how we style
unread counts, except for starred messages.
This is the common section.
diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css
index 1d942df20a..ff6d692a03 100644
--- a/web/styles/dark_theme.css
+++ b/web/styles/dark_theme.css
@@ -13,6 +13,19 @@
@extend .placeholder;
}
+ .demo-organization-warning {
+ background-color: hsl(0deg 60% 19%);
+ border-color: hsl(3deg 73% 74% / 40%);
+ color: hsl(3deg 73% 80%);
+
+ .convert-demo-organization-button {
+ &:focus {
+ color: hsl(200deg 79% 66%);
+ outline-color: hsl(200deg 79% 66%);
+ }
+ }
+ }
+
& a:hover {
color: hsl(200deg 79% 66%);
diff --git a/web/styles/settings.css b/web/styles/settings.css
index 0e25fcf956..91af685898 100644
--- a/web/styles/settings.css
+++ b/web/styles/settings.css
@@ -628,6 +628,12 @@ input[type="checkbox"] {
}
}
+#convert-demo-organization-form {
+ .domain_label {
+ display: inline-block;
+ }
+}
+
#profile-settings {
.custom-profile-fields-form .custom_user_field label,
.full-name-change-container label,
diff --git a/web/templates/settings/convert_demo_organization_form.hbs b/web/templates/settings/convert_demo_organization_form.hbs
new file mode 100644
index 0000000000..e4834c369b
--- /dev/null
+++ b/web/templates/settings/convert_demo_organization_form.hbs
@@ -0,0 +1,33 @@
+
{{t "You can convert this demo organization to a permanent Zulip organization. All users and message history will be preserved." }}
+ +