diff --git a/help-beta/astro.config.mjs b/help-beta/astro.config.mjs index d24a112e93..ebc1522aec 100644 --- a/help-beta/astro.config.mjs +++ b/help-beta/astro.config.mjs @@ -1,7 +1,7 @@ import * as fs from "node:fs"; import starlight from "@astrojs/starlight"; -import {defineConfig} from "astro/config"; +import {defineConfig, envField} from "astro/config"; import Icons from "unplugin-icons/vite"; // https://astro.build/config @@ -16,7 +16,7 @@ export default defineConfig({ // It was setting the height to 1024 and 960 for some // icons. It is better to set the height explicitly. defaultStyle: - "display: inline; vertical-align: text-bottom; height: 1em; width: 1em;", + "display: inline; vertical-align: text-bottom; height: 1em; width: 1em; margin-bottom: 0;", customCollections: { // unplugin-icons has a FileSystemIconLoader which is more // versatile. But it only supports one directory path for @@ -37,6 +37,16 @@ export default defineConfig({ }), ], }, + env: { + schema: { + SHOW_RELATIVE_LINKS: envField.boolean({ + context: "client", + access: "public", + optional: true, + default: true, + }), + }, + }, integrations: [ starlight({ title: "Zulip help center", diff --git a/help-beta/src/components/NavigationSteps.astro b/help-beta/src/components/NavigationSteps.astro new file mode 100644 index 0000000000..ecd52ad7a5 --- /dev/null +++ b/help-beta/src/components/NavigationSteps.astro @@ -0,0 +1,191 @@ +--- +import { SHOW_RELATIVE_LINKS } from "astro:env/client"; +import RawZulipIconGear from "~icons/zulip-icon/gear?raw"; + +// This list has been transformed one-off from `help_settings_links.py`, we +// have added a comment in that file to update this list in case of any +// changes. +const setting_link_mapping: { + [key: string]: { + setting_type: string, + setting_name: string, + setting_link: string, + } +} = { + // a mapping from the setting identifier that is the same as the final URL + // breadcrumb to that setting to the name of its setting type, the setting + // name as it appears in the user interface, and a relative link that can + // be used to get to that setting + "profile": { + setting_type: "Personal settings", + setting_name: "Profile", + setting_link: "/#settings/profile" + }, + "account-and-privacy": { + setting_type: "Personal settings", + setting_name: "Account & privacy", + setting_link: "/#settings/account-and-privacy" + }, + "preferences": { + setting_type: "Personal settings", + setting_name: "Preferences", + setting_link: "/#settings/preferences" + }, + "notifications": { + setting_type: "Personal settings", + setting_name: "Notifications", + setting_link: "/#settings/notifications" + }, + "your-bots": { + setting_type: "Personal settings", + setting_name: "Bots", + setting_link: "/#settings/your-bots" + }, + "alert-words": { + setting_type: "Personal settings", + setting_name: "Alert words", + setting_link: "/#settings/alert-words" + }, + "uploaded-files": { + setting_type: "Personal settings", + setting_name: "Uploaded files", + setting_link: "/#settings/uploaded-files" + }, + "topics": { + setting_type: "Personal settings", + setting_name: "Topics", + setting_link: "/#settings/topics" + }, + "muted-users": { + setting_type: "Personal settings", + setting_name: "Muted users", + setting_link: "/#settings/muted-users" + }, + "organization-profile": { + setting_type: "Organization settings", + setting_name: "Organization profile", + setting_link: "/#organization/organization-profile" + }, + "organization-settings": { + setting_type: "Organization settings", + setting_name: "Organization settings", + setting_link: "/#organization/organization-settings" + }, + "organization-permissions": { + setting_type: "Organization settings", + setting_name: "Organization permissions", + setting_link: "/#organization/organization-permissions" + }, + "default-user-settings": { + setting_type: "Organization settings", + setting_name: "Default user settings", + setting_link: "/#organization/organization-level-user-defaults" + }, + "emoji-settings": { + setting_type: "Organization settings", + setting_name: "Custom emoji", + setting_link: "/#organization/emoji-settings" + }, + "auth-methods": { + setting_type: "Organization settings", + setting_name: "Authentication methods", + setting_link: "/#organization/auth-methods" + }, + "users": { + setting_type: "Organization settings", + setting_name: "Users", + setting_link: "/#organization/users/active" + }, + "deactivated": { + setting_type: "Organization settings", + setting_name: "Users", + setting_link: "/#organization/users/deactivated" + }, + "invitations": { + setting_type: "Organization settings", + setting_name: "Users", + setting_link: "/#organization/users/invitations" + }, + "bot-list-admin": { + setting_type: "Organization settings", + setting_name: "Bots", + setting_link: "/#organization/bot-list-admin" + }, + "default-channels-list": { + setting_type: "Organization settings", + setting_name: "Default channels", + setting_link: "/#organization/default-channels-list" + }, + "linkifier-settings": { + setting_type: "Organization settings", + setting_name: "Linkifiers", + setting_link: "/#organization/linkifier-settings" + }, + "playground-settings": { + setting_type: "Organization settings", + setting_name: "Code playgrounds", + setting_link: "/#organization/playground-settings" + }, + "profile-field-settings": { + setting_type: "Organization settings", + setting_name: "Custom profile fields", + setting_link: "/#organization/profile-field-settings" + }, + "data-exports-admin": { + setting_type: "Organization settings", + setting_name: "Data exports", + setting_link: "/#organization/data-exports-admin" + } +}; + +const getSettingsMarkdown = (setting_type_name: string, setting_name: string) => ` +
    +
  1. + Click on the gear (${RawZulipIconGear}) icon in the upper + right corner of the web or desktop app. +
  2. +
  3. + Select ${setting_type_name}. +
  4. +
  5. + On the left, click ${setting_name}. +
  6. +
+` + +const getSettingsHTML = ( + setting_key: string, + SHOW_RELATIVE_LINKS: boolean +): string => { + const { + setting_type, + setting_name, + setting_link, + } = setting_link_mapping[setting_key]!; + + if (!SHOW_RELATIVE_LINKS) { + return getSettingsMarkdown(setting_type, setting_name); + } + + const relativeLink = `${setting_name}`; + + // The "Bots" label appears in both Personal and Organization settings + // in the user interface so we need special text for this setting. + const label = (setting_name === "Bots" || setting_name === "Users") + ? `Navigate to the ${relativeLink} tab of the ${setting_type} menu.` + : `Go to ${relativeLink}.`; + + return `
    +
  1. ${label}
  2. +
`; +} + +const { identifier } = Astro.props; +const navigation_link_type = identifier.split("/")[0]; +if (navigation_link_type !== "settings") { + throw new Error("Invalid navigation link type. Only `settings` is allowed."); +} +const resultHTML = getSettingsHTML(identifier.split("/")[1], SHOW_RELATIVE_LINKS); + +--- + diff --git a/tools/convert-help-center-docs-to-mdx b/tools/convert-help-center-docs-to-mdx index c8c8248869..816efa9806 100755 --- a/tools/convert-help-center-docs-to-mdx +++ b/tools/convert-help-center-docs-to-mdx @@ -25,6 +25,7 @@ django.setup() from zerver.lib.markdown.tabbed_sections import generate_content_blocks, parse_tabs INDENT_SPACES = " " +SETTINGS_LINK_PATTERN = re.compile(r"{settings_tab\|(?P.*?)}") # TODO list before cutover. # @@ -69,6 +70,21 @@ def replace_emoticon_translation_table(markdown_string: str, import_statement_se return result +def replace_setting_links( + markdown_string: str, import_statement_set: set[str], import_relative_base_path: str +) -> str: + setting_links_pattern = re.compile(r"{settings_tab\|(?P.*?)}") + + def replace_setting_links_match(match: re.Match[str]) -> str: + import_statement_set.add( + f'import NavigationSteps from "{import_relative_base_path}/NavigationSteps.astro"' + ) + setting_identifier = match.group("setting_identifier") + return f'' + + return setting_links_pattern.sub(replace_setting_links_match, markdown_string) + + def replace_image_path(markdown_string: str, replacement_path: str) -> str: """ We will point to the existing image folder till @@ -321,6 +337,10 @@ def insert_flattened_steps_component( if include_files_info[filename]["is_only_ordered_list"]: index += step continue + settings_link_match = SETTINGS_LINK_PATTERN.match(line) + if settings_link_match: + index += step + continue break return index @@ -330,11 +350,8 @@ def insert_flattened_steps_component( # resulting in two opening one after the other. # Using a set avoids this problem. insertions = set() - for match in file_include_pattern.finditer(markdown_string): - filename = match.group(1) - if not include_files_info[filename]["is_only_ordered_list"]: - continue + def insert_flatten_list(match: re.Match[str]) -> None: match_line_index = markdown_string[: match.start()].count("\n") upper_bound = traverse_to_boundary(match_line_index - 1, step=-1) @@ -343,6 +360,15 @@ def insert_flattened_steps_component( lower_bound = traverse_to_boundary(match_line_index + 1, step=1) insertions.add((lower_bound, "")) + for match in SETTINGS_LINK_PATTERN.finditer(markdown_string): + insert_flatten_list(match) + + for match in file_include_pattern.finditer(markdown_string): + filename = match.group(1) + if not include_files_info[filename]["is_only_ordered_list"]: + continue + insert_flatten_list(match) + if insertions: import_statement_set.add("import FlattenList from '../../components/FlattenList.astro';") # Insert tags in reverse order to avoid index shifting @@ -491,6 +517,7 @@ def convert_help_center_file_to_mdx( result = fix_file_imports(result, import_statement_set, "./include") result = convert_admonitions_to_asides(result, import_statement_set, "../../components") result = convert_tab_syntax(result, import_statement_set) + result = replace_setting_links(result, import_statement_set, "../../components") result = escape_curly_braces(result) result = fix_relative_path(result) result = replace_emoticon_translation_table(result, import_statement_set) @@ -527,6 +554,7 @@ def convert_include_file_to_mdx( result = fix_file_imports(result, import_statement_set, ".") result = convert_admonitions_to_asides(result, import_statement_set, "../../../components") result = convert_tab_syntax(result, import_statement_set) + result = replace_setting_links(result, import_statement_set, "../../../components") result = escape_curly_braces(result) result = fix_relative_path(result) result = replace_image_path(result, "../../../../../static/images/help") diff --git a/zerver/lib/markdown/help_settings_links.py b/zerver/lib/markdown/help_settings_links.py index 0ac75c9d49..bccda23f6c 100644 --- a/zerver/lib/markdown/help_settings_links.py +++ b/zerver/lib/markdown/help_settings_links.py @@ -15,6 +15,11 @@ from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITIES REGEXP = re.compile(r"\{settings_tab\|(?P.*?)\}") + +# If any changes to this link mapping are made, +# `help-beta/src/components/NavigationSteps.astro` should be updated accordingly. +# This manual update mechanism will cease to exist once we have switched to the +# help-beta system. link_mapping = { # a mapping from the setting identifier that is the same as the final URL # breadcrumb to that setting to the name of its setting type, the setting