From 7024e481e9d396a46c9388f97864a030c4abd500 Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Mon, 25 Aug 2025 13:27:00 +0200 Subject: [PATCH 01/20] #54: Move Settings to Options --- .../Paragraph}/Paragraph.tsx | 7 +- .../Paragraph}/index.ts | 0 .../TabNavigation/TabNavigation.tsx | 12 +- src/components/index.ts | 1 + src/pages/Options.tsx | 278 ++++++++++-------- src/pages/SettingsTab.tsx | 118 ++++++++ src/popup.tsx | 18 +- src/popup/Content.module.scss | 52 ---- src/popup/Content.tsx | 101 ------- src/popup/FormField/FormField.tsx | 2 +- src/popup/index.ts | 1 - 11 files changed, 287 insertions(+), 303 deletions(-) rename src/{popup/Typography => components/Paragraph}/Paragraph.tsx (62%) rename src/{popup/Typography => components/Paragraph}/index.ts (100%) create mode 100644 src/pages/SettingsTab.tsx delete mode 100644 src/popup/Content.module.scss delete mode 100644 src/popup/Content.tsx delete mode 100644 src/popup/index.ts diff --git a/src/popup/Typography/Paragraph.tsx b/src/components/Paragraph/Paragraph.tsx similarity index 62% rename from src/popup/Typography/Paragraph.tsx rename to src/components/Paragraph/Paragraph.tsx index 78ec0033..91129fb4 100644 --- a/src/popup/Typography/Paragraph.tsx +++ b/src/components/Paragraph/Paragraph.tsx @@ -1,11 +1,12 @@ import { Text } from "@primer/react"; -import React, { CSSProperties } from "react"; +import React, { CSSProperties, ReactNode } from "react"; type Props = { + children: ReactNode; sx?: CSSProperties; - children: React.ReactNode; }; -export const Paragraph: React.FC = ({ sx, children }) => { + +export const Paragraph: React.FC = ({ children, sx }) => { return ( void; +type Props = { + tabs: T[]; + activeTab: T; + onTabClick: (tab: T) => void; }; -export const TabNavigation: React.FC = ({ +export const TabNavigation = ({ tabs, activeTab, onTabClick, -}) => { +}: Props) => { return ( {tabs.map((tab) => ( diff --git a/src/components/index.ts b/src/components/index.ts index 661bac6a..0f1dc5bb 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,6 +2,7 @@ export * from "./ClosePopupButton"; export * from "./FeatureInput"; export * from "./FeatureItem"; export * from "./IconButton"; +export * from "./Paragraph"; export * from "./RandomReviewerButton"; export * from "./SearchInput"; export * from "./TabNavigation"; diff --git a/src/pages/Options.tsx b/src/pages/Options.tsx index 1bdb47b4..22d1ea75 100644 --- a/src/pages/Options.tsx +++ b/src/pages/Options.tsx @@ -1,14 +1,20 @@ import { Box, PageLayout, Text } from "@primer/react"; import { Banner } from "@primer/react/drafts"; import React, { useCallback, useEffect, useState } from "react"; -import { FeatureInput, FeatureItem } from "../components"; +import { FeatureInput, FeatureItem, TabNavigation } from "../components"; import { TemplateDescriptionParameters } from "../content"; import { Features, getSettings, INITIAL_VALUES } from "../services/getSettings"; import { ExportButton } from "./ExportButton"; import { ImportButton } from "./ImportButton"; +import { SettingsTab } from "./SettingsTab"; import styles from "./Options.module.scss"; +export type OptionsTab = "Settings" | "Feature Toggles"; + +const tabs: Array = ["Settings", "Feature Toggles"]; + export const Options = () => { + const [activeTab, setActiveTab] = useState("Settings"); const [features, setFeatures] = useState(INITIAL_VALUES.features); const [error, errorSet] = useState(); const [success, successSet] = useState(); @@ -20,7 +26,7 @@ export const Options = () => { setFeatures(settings.features); }, }), - [], + [] ); useEffect(() => { @@ -43,7 +49,7 @@ export const Options = () => { showError( err instanceof Error ? err.message - : "An error occurred while saving your settings", + : "An error occurred while saving your settings" ); } }; @@ -63,6 +69,145 @@ export const Options = () => { successSet(message); }; + const renderFeatureTogglesTab = () => ( + <> + + handleToggle("baseBranchLabels")} + ariaLabel="Toggle base branch labels" + /> + + handleToggle("changedFiles")} + ariaLabel="Toggle changed files" + /> + + handleToggle("totalLines")} + ariaLabel="Toggle total lines counter" + /> + + handleToggle("reOrderPrs")} + ariaLabel="Toggle reorder pull requests" + /> + + handleToggle("addUpdateBranchButton")} + ariaLabel="Toggle add update branch button" + /> + + handleToggle("autoFilter")} + ariaLabel="Toggle auto filter" + /> + + handleToggle("jira")} + ariaLabel="Toggle Jira integration" + /> + + handleToggle("randomReviewer")} + ariaLabel="Toggle assign random reviewer" + /> + + handleToggle("prTitleFromJira")} + ariaLabel="Toggle add PR title from Jira" + /> + + handleToggle("templateDescription")} + ariaLabel="Toggle template description" + /> + {features.templateDescription && ( + + )} + + + + Export & Import Settings + + + + You can export your current settings as a JSON file. Your settings + contain access tokens. Be careful and make sure to remove your tokens + before sharing. + + + + showSuccess("Exported settings successfully")} + onClick={resetBanners} + /> + { + loadSettings(); + showSuccess("Imported settings successfully"); + }} + onClick={resetBanners} + /> + + + ); + + const renderTabContent = () => { + switch (activeTab) { + case "Settings": + return ( + + ); + case "Feature Toggles": + return renderFeatureTogglesTab(); + } + }; + return ( { - - Features - - - - handleToggle("baseBranchLabels")} - ariaLabel="Toggle base branch labels" - /> - - handleToggle("changedFiles")} - ariaLabel="Toggle changed files" - /> - - handleToggle("totalLines")} - ariaLabel="Toggle total lines counter" - /> - - handleToggle("reOrderPrs")} - ariaLabel="Toggle reorder pull requests" - /> - - handleToggle("addUpdateBranchButton")} - ariaLabel="Toggle add update branch button" - /> - - handleToggle("autoFilter")} - ariaLabel="Toggle auto filter" - /> - - handleToggle("jira")} - ariaLabel="Toggle Jira integration" - /> - - handleToggle("randomReviewer")} - ariaLabel="Toggle assign random reviewer" - /> + - handleToggle("prTitleFromJira")} - ariaLabel="Toggle add PR title from Jira" - /> - - handleToggle("templateDescription")} - ariaLabel="Toggle template description" - /> - {features.templateDescription && ( - - )} - - - - Export & Import Settings - - - - You can export your current settings as a JSON file. Your settings - contain access tokens. Be careful and make sure to remove your - tokens before sharing. - - - - showSuccess("Exported settings successfully")} - onClick={resetBanners} - /> - { - loadSettings(); - showSuccess("Imported settings successfully"); - }} - onClick={resetBanners} - /> - + {renderTabContent()} diff --git a/src/pages/SettingsTab.tsx b/src/pages/SettingsTab.tsx new file mode 100644 index 00000000..e4028f97 --- /dev/null +++ b/src/pages/SettingsTab.tsx @@ -0,0 +1,118 @@ +import { Box, Text } from "@primer/react"; +import { Form, Formik, FormikHelpers } from "formik"; +import React, { useEffect, useState } from "react"; +import { TabNavigation, Tab } from "../components"; +import { Features } from "../services/getSettings"; +import { + INITIAL_VALUES, + Settings, + getSettings, + persistSettings, + settingsSchema, +} from "../services"; +import { SubmitButton } from "../popup/Button"; +import { GhInstancesTab, JiraTab } from "../popup/Tabs"; +import { AutoFilterTab } from "../popup/Tabs/AutoFilterTab"; +import styles from "./Options.module.scss"; + +const tabs: Array = ["GH Instances", "Auto filter", "Jira"]; + +type Props = { + features: Features; + onError: (message?: string) => void; + onSuccess: (message: string) => void; + onReset: () => void; +}; + +export const SettingsTab: React.FC = ({ + features, + onError, + onSuccess, + onReset: _, +}) => { + const [activeTab, setActiveTab] = useState("GH Instances"); + const [result, resultSet] = useState(""); + const [initialValues, initialValuesSet] = useState(INITIAL_VALUES); + + useEffect(() => { + getSettings({ + onSuccess: initialValuesSet, + onError: () => onError("Couldn't load from chrome storage"), + }); + }, [onError]); + + const handleSubmit = ( + values: Settings, + { setSubmitting, resetForm }: FormikHelpers, + ) => + persistSettings({ + values, + onSuccess: () => { + resetForm({ values }); + resultSet("Saved successfully"); + onSuccess("Settings saved successfully"); + }, + onError: () => { + resultSet("Couldn't save"); + onError("Couldn't save settings"); + }, + onSettled: () => setSubmitting(false), + }); + + const mapTabToComponent = (tab: Tab, values: Settings, isValid: boolean) => { + switch (tab) { + case "Auto filter": + return ; + case "GH Instances": + return ; + case "Jira": + return ; + } + }; + + return ( + + + Settings + + + + Configure GitHub instances, auto-filtering, and Jira integration. + + + + + + + + {({ isValid, dirty, isSubmitting, values }) => { + return ( +
+ {mapTabToComponent(activeTab, values, isValid)} + + + +
+ ); + }} +
+
+
+
+ ); +}; diff --git a/src/popup.tsx b/src/popup.tsx index d8790072..ab270a3c 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -1,15 +1,3 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import { Content } from "./popup/Content"; -import "./styles.scss"; -import { ThemeProvider } from "@primer/react"; - -const root = createRoot(document.getElementById("root")!); - -root.render( - - - - - , -); +// Immediately open options page and close popup +void chrome.tabs.create({ url: chrome.runtime.getURL("options.html") }); +window.close(); diff --git a/src/popup/Content.module.scss b/src/popup/Content.module.scss deleted file mode 100644 index e8c0f863..00000000 --- a/src/popup/Content.module.scss +++ /dev/null @@ -1,52 +0,0 @@ -.wrapper { - min-width: 450px; - max-height: 560px; - background-color: rgb(13, 17, 23); - display: flex; - flex-direction: column; -} - -.container { - background-color: rgb(22, 27, 34); - border-bottom-color: rgb(48, 54, 61); - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; - border-bottom-style: solid; - border-bottom-width: 0.8px; - border-left-color: rgb(48, 54, 61); - border-left-style: solid; - border-left-width: 0.8px; - border-right-color: rgb(48, 54, 61); - border-right-style: solid; - border-right-width: 0.8px; - border-top-color: rgb(48, 54, 61); - border-top-left-radius: 6px; - border-top-right-radius: 6px; - border-top-style: solid; - border-top-width: 0.8px; - box-sizing: border-box; -} - -.headingContainer { - display: flex; - justify-content: center; - flex-direction: column; -} - -.form { - padding: 1rem 2rem; -} - -$fontColor: rgba(255, 255, 255); - -.heading { - color: $fontColor; - font-size: 20px; - font-weight: 400; - padding: 1rem; -} - -.divider { - border-bottom: 0.8px solid rgb(33, 38, 45); -} - diff --git a/src/popup/Content.tsx b/src/popup/Content.tsx deleted file mode 100644 index 36bf00bf..00000000 --- a/src/popup/Content.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Link } from "@primer/react"; -import { Form, Formik, FormikHelpers } from "formik"; -import React, { useEffect, useState } from "react"; -import { Tab, TabNavigation } from "../components"; -import { - INITIAL_VALUES, - Settings, - getSettings, - persistSettings, - settingsSchema, -} from "../services"; -import { SubmitButton } from "./Button"; -import styles from "./Content.module.scss"; -import { GhInstancesTab, JiraTab } from "./Tabs"; -import { AutoFilterTab } from "./Tabs/AutoFilterTab"; -import { Paragraph } from "./Typography"; - -const tabs: Array = ["GH Instances", "Auto filter", "Jira"]; - -export const Content = () => { - const [activeTab, setActiveTab] = useState("GH Instances"); - const [result, resultSet] = useState(""); - const [initialValues, initialValuesSet] = useState(INITIAL_VALUES); - - useEffect(() => { - getSettings({ - onSuccess: initialValuesSet, - onError: () => resultSet("Couldn't load from chrome storage"), - }); - // eslint-disable-next-line react-hooks-addons/no-unused-deps - }, [initialValues]); - - const handleSubmit = ( - values: Settings, - { setSubmitting, resetForm }: FormikHelpers, - ) => - persistSettings({ - values, - onSuccess: () => { - // reset form-state, e.g. isDirty - resetForm({ values }); - resultSet("Saved successfully"); - }, - onError: () => resultSet("Couldn't save"), - onSettled: () => setSubmitting(false), - }); - - const mapTabToComponent = (tab: Tab, values: Settings, isValid: boolean) => { - switch (tab) { - case "Auto filter": - return ; - case "GH Instances": - return ; - case "Jira": - return ; - } - }; - - return ( -
-
-
-

GitHub UI Booster - Settings

- - Enable/disable features in the{" "} - - options page - - -
-
- - - {({ isValid, dirty, isSubmitting, values }) => { - return ( -
- {mapTabToComponent(activeTab, values, isValid)} - - - ); - }} -
-
-
- ); -}; diff --git a/src/popup/FormField/FormField.tsx b/src/popup/FormField/FormField.tsx index f309d38e..86a76f96 100644 --- a/src/popup/FormField/FormField.tsx +++ b/src/popup/FormField/FormField.tsx @@ -1,7 +1,7 @@ import { ErrorMessage, Field } from "formik"; import React from "react"; +import { Paragraph } from "../../components"; import { SettingName } from "../../services"; -import { Paragraph } from "../Typography"; import styles from "./FormField.module.scss"; type Props = { diff --git a/src/popup/index.ts b/src/popup/index.ts deleted file mode 100644 index 7f2950d0..00000000 --- a/src/popup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Content } from "./Content"; From 26454b64bbb8bc457645e0bdc70e7a268e0e208c Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Mon, 25 Aug 2025 13:44:30 +0200 Subject: [PATCH 02/20] #54: Refactor and modularize --- .../SectionTitle/SectionTitle.module.scss | 9 + src/components/SectionTitle/SectionTitle.tsx | 15 ++ src/components/SectionTitle/index.ts | 1 + src/components/Subtitle/Subtitle.module.scss | 4 + src/components/Subtitle/Subtitle.tsx | 15 ++ src/components/Subtitle/index.ts | 1 + src/components/index.ts | 2 + src/pages/FeatureToggles.tsx | 154 ++++++++++++++++++ src/pages/Options.module.scss | 15 -- src/pages/Options.tsx | 148 ++--------------- src/pages/SettingsTab.tsx | 13 +- .../GhInstancesTab/GhInstancesTab.module.scss | 4 - .../Tabs/GhInstancesTab/GhInstancesTab.tsx | 7 +- 13 files changed, 224 insertions(+), 164 deletions(-) create mode 100644 src/components/SectionTitle/SectionTitle.module.scss create mode 100644 src/components/SectionTitle/SectionTitle.tsx create mode 100644 src/components/SectionTitle/index.ts create mode 100644 src/components/Subtitle/Subtitle.module.scss create mode 100644 src/components/Subtitle/Subtitle.tsx create mode 100644 src/components/Subtitle/index.ts create mode 100644 src/pages/FeatureToggles.tsx diff --git a/src/components/SectionTitle/SectionTitle.module.scss b/src/components/SectionTitle/SectionTitle.module.scss new file mode 100644 index 00000000..6ba39ee7 --- /dev/null +++ b/src/components/SectionTitle/SectionTitle.module.scss @@ -0,0 +1,9 @@ +@import "../../shared-component-styles.scss"; + +.sectionTitle { + color: $fontColor; + font-size: 20px; + font-weight: bold; + margin-bottom: 24px; + margin-top: 24px; +} diff --git a/src/components/SectionTitle/SectionTitle.tsx b/src/components/SectionTitle/SectionTitle.tsx new file mode 100644 index 00000000..dc7b2274 --- /dev/null +++ b/src/components/SectionTitle/SectionTitle.tsx @@ -0,0 +1,15 @@ +import { Text } from "@primer/react"; +import React, { ReactNode } from "react"; +import styles from "./SectionTitle.module.scss"; + +type Props = { + children: ReactNode; +}; + +export const SectionTitle: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/src/components/SectionTitle/index.ts b/src/components/SectionTitle/index.ts new file mode 100644 index 00000000..26a2bb52 --- /dev/null +++ b/src/components/SectionTitle/index.ts @@ -0,0 +1 @@ +export { SectionTitle } from "./SectionTitle"; diff --git a/src/components/Subtitle/Subtitle.module.scss b/src/components/Subtitle/Subtitle.module.scss new file mode 100644 index 00000000..dd852f5c --- /dev/null +++ b/src/components/Subtitle/Subtitle.module.scss @@ -0,0 +1,4 @@ +.subtitle { + font-size: 14px; + font-style: italic; +} diff --git a/src/components/Subtitle/Subtitle.tsx b/src/components/Subtitle/Subtitle.tsx new file mode 100644 index 00000000..aafc72ee --- /dev/null +++ b/src/components/Subtitle/Subtitle.tsx @@ -0,0 +1,15 @@ +import { Text } from "@primer/react"; +import React, { ReactNode } from "react"; +import styles from "./Subtitle.module.scss"; + +type Props = { + children: ReactNode; +}; + +export const Subtitle: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/src/components/Subtitle/index.ts b/src/components/Subtitle/index.ts new file mode 100644 index 00000000..366bcf9c --- /dev/null +++ b/src/components/Subtitle/index.ts @@ -0,0 +1 @@ +export { Subtitle } from "./Subtitle"; diff --git a/src/components/index.ts b/src/components/index.ts index 0f1dc5bb..b4e36d1a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,6 +5,8 @@ export * from "./IconButton"; export * from "./Paragraph"; export * from "./RandomReviewerButton"; export * from "./SearchInput"; +export * from "./SectionTitle"; +export * from "./Subtitle"; export * from "./TabNavigation"; export * from "./TotalLines"; export * from "./UpdateBranchButton"; diff --git a/src/pages/FeatureToggles.tsx b/src/pages/FeatureToggles.tsx new file mode 100644 index 00000000..44daae72 --- /dev/null +++ b/src/pages/FeatureToggles.tsx @@ -0,0 +1,154 @@ +import { Box } from "@primer/react"; +import React from "react"; +import { + FeatureInput, + FeatureItem, + SectionTitle, + Subtitle, +} from "../components"; +import { TemplateDescriptionParameters } from "../content"; +import { Features } from "../services/getSettings"; +import { ExportButton } from "./ExportButton"; +import { ImportButton } from "./ImportButton"; +import styles from "./Options.module.scss"; + +type Props = { + features: Features; + onToggle: (key: keyof Features) => void; + onError: (message?: string) => void; + onSuccess: (message: string) => void; + onReset: () => void; + onLoadSettings: () => void; +}; + +export const FeatureToggles: React.FC = ({ + features, + onToggle, + onError, + onSuccess, + onReset, + onLoadSettings, +}) => { + return ( + <> + + Enable and disable Extension-features. + + onToggle("baseBranchLabels")} + ariaLabel="Toggle base branch labels" + /> + + onToggle("changedFiles")} + ariaLabel="Toggle changed files" + /> + + onToggle("totalLines")} + ariaLabel="Toggle total lines counter" + /> + + onToggle("reOrderPrs")} + ariaLabel="Toggle reorder pull requests" + /> + + onToggle("addUpdateBranchButton")} + ariaLabel="Toggle add update branch button" + /> + + onToggle("autoFilter")} + ariaLabel="Toggle auto filter" + /> + + onToggle("jira")} + ariaLabel="Toggle Jira integration" + /> + + onToggle("randomReviewer")} + ariaLabel="Toggle assign random reviewer" + /> + + onToggle("prTitleFromJira")} + ariaLabel="Toggle add PR title from Jira" + /> + + onToggle("templateDescription")} + ariaLabel="Toggle template description" + /> + {features.templateDescription && ( + + )} + + + Export & Import Settings + + + You can export your current settings as a JSON file. Your settings + contain access tokens. Be careful and make sure to remove your tokens + before sharing. + + + + onSuccess("Exported settings successfully")} + onClick={onReset} + /> + { + onLoadSettings(); + onSuccess("Imported settings successfully"); + }} + onClick={onReset} + /> + + + ); +}; diff --git a/src/pages/Options.module.scss b/src/pages/Options.module.scss index 332d3cd8..ee565377 100644 --- a/src/pages/Options.module.scss +++ b/src/pages/Options.module.scss @@ -1,4 +1,3 @@ - .container { width: 100%; height: 100%; @@ -31,22 +30,8 @@ margin-bottom: 2px; } -.subtitle { - font-size: 14px; - font-style: italic; -} - -.sectionTitle { - font-size: 20px; - font-weight: bold; - margin-bottom: 24px; - margin-top: 24px; -} - .featuresList { display: flex; flex-direction: column; gap: 24px; } - - diff --git a/src/pages/Options.tsx b/src/pages/Options.tsx index 22d1ea75..5f7e8c7a 100644 --- a/src/pages/Options.tsx +++ b/src/pages/Options.tsx @@ -1,11 +1,9 @@ import { Box, PageLayout, Text } from "@primer/react"; import { Banner } from "@primer/react/drafts"; import React, { useCallback, useEffect, useState } from "react"; -import { FeatureInput, FeatureItem, TabNavigation } from "../components"; -import { TemplateDescriptionParameters } from "../content"; +import { TabNavigation, Subtitle } from "../components"; import { Features, getSettings, INITIAL_VALUES } from "../services/getSettings"; -import { ExportButton } from "./ExportButton"; -import { ImportButton } from "./ImportButton"; +import { FeatureToggles } from "./FeatureToggles"; import { SettingsTab } from "./SettingsTab"; import styles from "./Options.module.scss"; @@ -26,7 +24,7 @@ export const Options = () => { setFeatures(settings.features); }, }), - [] + [], ); useEffect(() => { @@ -49,7 +47,7 @@ export const Options = () => { showError( err instanceof Error ? err.message - : "An error occurred while saving your settings" + : "An error occurred while saving your settings", ); } }; @@ -69,129 +67,6 @@ export const Options = () => { successSet(message); }; - const renderFeatureTogglesTab = () => ( - <> - - handleToggle("baseBranchLabels")} - ariaLabel="Toggle base branch labels" - /> - - handleToggle("changedFiles")} - ariaLabel="Toggle changed files" - /> - - handleToggle("totalLines")} - ariaLabel="Toggle total lines counter" - /> - - handleToggle("reOrderPrs")} - ariaLabel="Toggle reorder pull requests" - /> - - handleToggle("addUpdateBranchButton")} - ariaLabel="Toggle add update branch button" - /> - - handleToggle("autoFilter")} - ariaLabel="Toggle auto filter" - /> - - handleToggle("jira")} - ariaLabel="Toggle Jira integration" - /> - - handleToggle("randomReviewer")} - ariaLabel="Toggle assign random reviewer" - /> - - handleToggle("prTitleFromJira")} - ariaLabel="Toggle add PR title from Jira" - /> - - handleToggle("templateDescription")} - ariaLabel="Toggle template description" - /> - {features.templateDescription && ( - - )} - - - - Export & Import Settings - - - - You can export your current settings as a JSON file. Your settings - contain access tokens. Be careful and make sure to remove your tokens - before sharing. - - - - showSuccess("Exported settings successfully")} - onClick={resetBanners} - /> - { - loadSettings(); - showSuccess("Imported settings successfully"); - }} - onClick={resetBanners} - /> - - - ); - const renderTabContent = () => { switch (activeTab) { case "Settings": @@ -204,7 +79,16 @@ export const Options = () => { /> ); case "Feature Toggles": - return renderFeatureTogglesTab(); + return ( + + ); } }; @@ -229,10 +113,10 @@ export const Options = () => { GitHub UI Booster Options - + Making your GitHub experience smoother than a freshly polished commit πŸš€ - + diff --git a/src/pages/SettingsTab.tsx b/src/pages/SettingsTab.tsx index e4028f97..c642484c 100644 --- a/src/pages/SettingsTab.tsx +++ b/src/pages/SettingsTab.tsx @@ -1,7 +1,7 @@ -import { Box, Text } from "@primer/react"; +import { Box } from "@primer/react"; import { Form, Formik, FormikHelpers } from "formik"; import React, { useEffect, useState } from "react"; -import { TabNavigation, Tab } from "../components"; +import { TabNavigation, Tab, Subtitle } from "../components"; import { Features } from "../services/getSettings"; import { INITIAL_VALUES, @@ -13,7 +13,6 @@ import { import { SubmitButton } from "../popup/Button"; import { GhInstancesTab, JiraTab } from "../popup/Tabs"; import { AutoFilterTab } from "../popup/Tabs/AutoFilterTab"; -import styles from "./Options.module.scss"; const tabs: Array = ["GH Instances", "Auto filter", "Jira"]; @@ -72,13 +71,9 @@ export const SettingsTab: React.FC = ({ return ( - - Settings - - - + Configure GitHub instances, auto-filtering, and Jira integration. - + { values.instances.map((_, index) => ( - - {`GH Instance ${index + 1}`} - + {`GH Instance ${index + 1}`} remove(index)} From dd7a6757ee325fc8c0330e59df59562fc116f2f8 Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Mon, 25 Aug 2025 14:04:17 +0200 Subject: [PATCH 03/20] #54: Remove SettingsTab --- ...atureToggles.tsx => FeatureTogglesTab.tsx} | 2 +- src/pages/Options.tsx | 87 +++++++++++--- src/pages/SettingsTab.tsx | 113 ------------------ .../Tabs/AutoFilterTab/AutoFilterTab.tsx | 16 ++- .../Tabs/GhInstancesTab/GhInstancesTab.tsx | 97 ++++++++------- src/popup/Tabs/JiraTab/JiraTab.tsx | 4 + 6 files changed, 139 insertions(+), 180 deletions(-) rename src/pages/{FeatureToggles.tsx => FeatureTogglesTab.tsx} (98%) delete mode 100644 src/pages/SettingsTab.tsx diff --git a/src/pages/FeatureToggles.tsx b/src/pages/FeatureTogglesTab.tsx similarity index 98% rename from src/pages/FeatureToggles.tsx rename to src/pages/FeatureTogglesTab.tsx index 44daae72..4966b07d 100644 --- a/src/pages/FeatureToggles.tsx +++ b/src/pages/FeatureTogglesTab.tsx @@ -21,7 +21,7 @@ type Props = { onLoadSettings: () => void; }; -export const FeatureToggles: React.FC = ({ +export const FeatureTogglesTab: React.FC = ({ features, onToggle, onError, diff --git a/src/pages/Options.tsx b/src/pages/Options.tsx index 5f7e8c7a..c6b63ba4 100644 --- a/src/pages/Options.tsx +++ b/src/pages/Options.tsx @@ -1,27 +1,43 @@ import { Box, PageLayout, Text } from "@primer/react"; import { Banner } from "@primer/react/drafts"; +import { Form, Formik, FormikHelpers } from "formik"; import React, { useCallback, useEffect, useState } from "react"; import { TabNavigation, Subtitle } from "../components"; import { Features, getSettings, INITIAL_VALUES } from "../services/getSettings"; -import { FeatureToggles } from "./FeatureToggles"; -import { SettingsTab } from "./SettingsTab"; +import { Settings, persistSettings, settingsSchema } from "../services"; +import { SubmitButton } from "../popup/Button"; +import { GhInstancesTab, JiraTab } from "../popup/Tabs"; +import { AutoFilterTab } from "../popup/Tabs/AutoFilterTab"; +import { FeatureTogglesTab } from "./FeatureTogglesTab"; import styles from "./Options.module.scss"; -export type OptionsTab = "Settings" | "Feature Toggles"; +export type OptionsTab = + | "Feature Toggles" + | "GH Instances" + | "Auto filter" + | "Jira"; -const tabs: Array = ["Settings", "Feature Toggles"]; +const tabs: Array = [ + "Feature Toggles", + "GH Instances", + "Auto filter", + "Jira", +]; export const Options = () => { - const [activeTab, setActiveTab] = useState("Settings"); + const [activeTab, setActiveTab] = useState("Feature Toggles"); const [features, setFeatures] = useState(INITIAL_VALUES.features); const [error, errorSet] = useState(); const [success, successSet] = useState(); + const [initialValues, initialValuesSet] = useState(INITIAL_VALUES); + const [result, resultSet] = useState(""); const loadSettings = useCallback( () => getSettings({ onSuccess: (settings) => { setFeatures(settings.features); + initialValuesSet(settings); }, }), [], @@ -67,20 +83,29 @@ export const Options = () => { successSet(message); }; + const handleSubmit = ( + values: Settings, + { setSubmitting, resetForm }: FormikHelpers, + ) => + persistSettings({ + values, + onSuccess: () => { + resetForm({ values }); + resultSet("Saved successfully"); + showSuccess("Settings saved successfully"); + }, + onError: () => { + resultSet("Couldn't save"); + showError("Couldn't save settings"); + }, + onSettled: () => setSubmitting(false), + }); + const renderTabContent = () => { switch (activeTab) { - case "Settings": - return ( - - ); case "Feature Toggles": return ( - { onLoadSettings={loadSettings} /> ); + case "GH Instances": + case "Auto filter": + case "Jira": + return ( + + {({ isValid, dirty, isSubmitting, values }) => ( +
+ {activeTab === "GH Instances" && ( + + )} + {activeTab === "Auto filter" && ( + + )} + {activeTab === "Jira" && } + + + + + )} +
+ ); } }; diff --git a/src/pages/SettingsTab.tsx b/src/pages/SettingsTab.tsx deleted file mode 100644 index c642484c..00000000 --- a/src/pages/SettingsTab.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Box } from "@primer/react"; -import { Form, Formik, FormikHelpers } from "formik"; -import React, { useEffect, useState } from "react"; -import { TabNavigation, Tab, Subtitle } from "../components"; -import { Features } from "../services/getSettings"; -import { - INITIAL_VALUES, - Settings, - getSettings, - persistSettings, - settingsSchema, -} from "../services"; -import { SubmitButton } from "../popup/Button"; -import { GhInstancesTab, JiraTab } from "../popup/Tabs"; -import { AutoFilterTab } from "../popup/Tabs/AutoFilterTab"; - -const tabs: Array = ["GH Instances", "Auto filter", "Jira"]; - -type Props = { - features: Features; - onError: (message?: string) => void; - onSuccess: (message: string) => void; - onReset: () => void; -}; - -export const SettingsTab: React.FC = ({ - features, - onError, - onSuccess, - onReset: _, -}) => { - const [activeTab, setActiveTab] = useState("GH Instances"); - const [result, resultSet] = useState(""); - const [initialValues, initialValuesSet] = useState(INITIAL_VALUES); - - useEffect(() => { - getSettings({ - onSuccess: initialValuesSet, - onError: () => onError("Couldn't load from chrome storage"), - }); - }, [onError]); - - const handleSubmit = ( - values: Settings, - { setSubmitting, resetForm }: FormikHelpers, - ) => - persistSettings({ - values, - onSuccess: () => { - resetForm({ values }); - resultSet("Saved successfully"); - onSuccess("Settings saved successfully"); - }, - onError: () => { - resultSet("Couldn't save"); - onError("Couldn't save settings"); - }, - onSettled: () => setSubmitting(false), - }); - - const mapTabToComponent = (tab: Tab, values: Settings, isValid: boolean) => { - switch (tab) { - case "Auto filter": - return ; - case "GH Instances": - return ; - case "Jira": - return ; - } - }; - - return ( - - - Configure GitHub instances, auto-filtering, and Jira integration. - - - - - - - - {({ isValid, dirty, isSubmitting, values }) => { - return ( -
- {mapTabToComponent(activeTab, values, isValid)} - - - -
- ); - }} -
-
-
-
- ); -}; diff --git a/src/popup/Tabs/AutoFilterTab/AutoFilterTab.tsx b/src/popup/Tabs/AutoFilterTab/AutoFilterTab.tsx index 2bd8c6ec..7beefbbf 100644 --- a/src/popup/Tabs/AutoFilterTab/AutoFilterTab.tsx +++ b/src/popup/Tabs/AutoFilterTab/AutoFilterTab.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Subtitle } from "../../../components"; import { FormField } from "../../FormField"; type Props = { @@ -7,11 +8,14 @@ type Props = { export const AutoFilterTab: React.FC = ({ disabled }) => { return ( - + <> + Set up automatic filters for pull requests. + + ); }; diff --git a/src/popup/Tabs/GhInstancesTab/GhInstancesTab.tsx b/src/popup/Tabs/GhInstancesTab/GhInstancesTab.tsx index fdfa533b..3b238e74 100644 --- a/src/popup/Tabs/GhInstancesTab/GhInstancesTab.tsx +++ b/src/popup/Tabs/GhInstancesTab/GhInstancesTab.tsx @@ -2,7 +2,7 @@ import { Pagehead } from "@primer/react"; import { FieldArray } from "formik"; import React from "react"; import { isNonEmptyArray } from "ts-type-safe"; -import { SectionTitle } from "../../../components"; +import { SectionTitle, Subtitle } from "../../../components"; import { Settings } from "../../../services"; import { AddButton, RemoveButton } from "../../Button"; import { FormField } from "../../FormField"; @@ -15,52 +15,59 @@ type Props = { export const GhInstancesTab = ({ values, isValid }: Props) => { return ( - - {/* eslint-disable-next-line @typescript-eslint/unbound-method */} - {({ push, remove }) => ( - <> - {isNonEmptyArray(values.instances) && - values.instances.map((_, index) => ( - - - {`GH Instance ${index + 1}`} - remove(index)} + <> + + Configure GitHub instances with API tokens and repository access. + + + {(arrayHelpers) => ( + <> + {isNonEmptyArray(values.instances) && + values.instances.map((_, index) => ( + + + {`GH Instance ${index + 1}`} + arrayHelpers.remove(index)} + /> + + + + - - - - - - {values.features.randomReviewer && ( - )} - - ))} - - - )} - + {values.features.randomReviewer && ( + + )} +
+ ))} + arrayHelpers.push(obj)} + /> + + )} + + ); }; diff --git a/src/popup/Tabs/JiraTab/JiraTab.tsx b/src/popup/Tabs/JiraTab/JiraTab.tsx index 802ad8ac..bb28a9bc 100644 --- a/src/popup/Tabs/JiraTab/JiraTab.tsx +++ b/src/popup/Tabs/JiraTab/JiraTab.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Subtitle } from "../../../components"; import { FormField } from "../../FormField"; type Props = { @@ -8,6 +9,9 @@ type Props = { export const JiraTab: React.FC = ({ disabled }) => { return ( <> + + Configure Jira integration for automatic issue linking and PR titles. + Date: Mon, 25 Aug 2025 14:13:05 +0200 Subject: [PATCH 04/20] #54: Move files from popup to pages --- src/{popup => pages}/Button/AddButton.tsx | 0 src/{popup => pages}/Button/Button.module.scss | 0 src/{popup => pages}/Button/Button.tsx | 0 src/pages/{ => Button}/ExportButton.tsx | 4 ++-- src/pages/{ => Button}/ImportButton.tsx | 5 ++--- src/{popup => pages}/Button/RemoveButton.tsx | 0 src/{popup => pages}/Button/SubmitButton.tsx | 0 src/{popup => pages}/Button/index.ts | 2 ++ src/{popup => pages}/FormField/FormField.module.scss | 0 src/{popup => pages}/FormField/FormField.tsx | 0 src/{popup => pages}/FormField/index.ts | 0 src/pages/Options.tsx | 8 ++++---- .../Tabs/AutoFilterTab/AutoFilterTab.tsx | 0 src/{popup => pages}/Tabs/AutoFilterTab/index.ts | 0 .../{ => Tabs/FeatureTogglesTab}/FeatureTogglesTab.tsx | 9 ++++----- src/pages/Tabs/FeatureTogglesTab/index.ts | 1 + .../Tabs/GhInstancesTab/GhInstancesTab.module.scss | 0 .../Tabs/GhInstancesTab/GhInstancesTab.tsx | 0 src/{popup => pages}/Tabs/GhInstancesTab/index.ts | 0 src/{popup => pages}/Tabs/JiraTab/JiraTab.tsx | 0 src/{popup => pages}/Tabs/JiraTab/index.ts | 0 src/{popup => pages}/Tabs/index.ts | 1 + 22 files changed, 16 insertions(+), 14 deletions(-) rename src/{popup => pages}/Button/AddButton.tsx (100%) rename src/{popup => pages}/Button/Button.module.scss (100%) rename src/{popup => pages}/Button/Button.tsx (100%) rename src/pages/{ => Button}/ExportButton.tsx (95%) rename src/pages/{ => Button}/ImportButton.tsx (92%) rename src/{popup => pages}/Button/RemoveButton.tsx (100%) rename src/{popup => pages}/Button/SubmitButton.tsx (100%) rename src/{popup => pages}/Button/index.ts (64%) rename src/{popup => pages}/FormField/FormField.module.scss (100%) rename src/{popup => pages}/FormField/FormField.tsx (100%) rename src/{popup => pages}/FormField/index.ts (100%) rename src/{popup => pages}/Tabs/AutoFilterTab/AutoFilterTab.tsx (100%) rename src/{popup => pages}/Tabs/AutoFilterTab/index.ts (100%) rename src/pages/{ => Tabs/FeatureTogglesTab}/FeatureTogglesTab.tsx (95%) create mode 100644 src/pages/Tabs/FeatureTogglesTab/index.ts rename src/{popup => pages}/Tabs/GhInstancesTab/GhInstancesTab.module.scss (100%) rename src/{popup => pages}/Tabs/GhInstancesTab/GhInstancesTab.tsx (100%) rename src/{popup => pages}/Tabs/GhInstancesTab/index.ts (100%) rename src/{popup => pages}/Tabs/JiraTab/JiraTab.tsx (100%) rename src/{popup => pages}/Tabs/JiraTab/index.ts (100%) rename src/{popup => pages}/Tabs/index.ts (71%) diff --git a/src/popup/Button/AddButton.tsx b/src/pages/Button/AddButton.tsx similarity index 100% rename from src/popup/Button/AddButton.tsx rename to src/pages/Button/AddButton.tsx diff --git a/src/popup/Button/Button.module.scss b/src/pages/Button/Button.module.scss similarity index 100% rename from src/popup/Button/Button.module.scss rename to src/pages/Button/Button.module.scss diff --git a/src/popup/Button/Button.tsx b/src/pages/Button/Button.tsx similarity index 100% rename from src/popup/Button/Button.tsx rename to src/pages/Button/Button.tsx diff --git a/src/pages/ExportButton.tsx b/src/pages/Button/ExportButton.tsx similarity index 95% rename from src/pages/ExportButton.tsx rename to src/pages/Button/ExportButton.tsx index 1af34fb3..f721fdc8 100644 --- a/src/pages/ExportButton.tsx +++ b/src/pages/Button/ExportButton.tsx @@ -1,7 +1,7 @@ import { DownloadIcon } from "@primer/octicons-react"; import React from "react"; -import { Button } from "../popup/Button"; -import { getSettings } from "../services/getSettings"; +import { Button } from "./Button"; +import { getSettings } from "../../services/getSettings"; type Props = { onSuccess?: () => void; diff --git a/src/pages/ImportButton.tsx b/src/pages/Button/ImportButton.tsx similarity index 92% rename from src/pages/ImportButton.tsx rename to src/pages/Button/ImportButton.tsx index 0367109c..51aed2cf 100644 --- a/src/pages/ImportButton.tsx +++ b/src/pages/Button/ImportButton.tsx @@ -1,8 +1,7 @@ import { UploadIcon } from "@primer/octicons-react"; import React, { useRef, useState } from "react"; -import { Button } from "../popup/Button"; -import { persistSettings } from "../services"; -import { settingsSchema } from "../services/getSettings"; +import { Button } from "./Button"; +import { persistSettings, settingsSchema } from "../../services"; type Props = { onSuccess?: () => void; diff --git a/src/popup/Button/RemoveButton.tsx b/src/pages/Button/RemoveButton.tsx similarity index 100% rename from src/popup/Button/RemoveButton.tsx rename to src/pages/Button/RemoveButton.tsx diff --git a/src/popup/Button/SubmitButton.tsx b/src/pages/Button/SubmitButton.tsx similarity index 100% rename from src/popup/Button/SubmitButton.tsx rename to src/pages/Button/SubmitButton.tsx diff --git a/src/popup/Button/index.ts b/src/pages/Button/index.ts similarity index 64% rename from src/popup/Button/index.ts rename to src/pages/Button/index.ts index 4b342589..cf36fd25 100644 --- a/src/popup/Button/index.ts +++ b/src/pages/Button/index.ts @@ -1,4 +1,6 @@ export { AddButton } from "./AddButton"; export { Button } from "./Button"; +export { ExportButton } from "./ExportButton"; +export { ImportButton } from "./ImportButton"; export { RemoveButton } from "./RemoveButton"; export { SubmitButton } from "./SubmitButton"; diff --git a/src/popup/FormField/FormField.module.scss b/src/pages/FormField/FormField.module.scss similarity index 100% rename from src/popup/FormField/FormField.module.scss rename to src/pages/FormField/FormField.module.scss diff --git a/src/popup/FormField/FormField.tsx b/src/pages/FormField/FormField.tsx similarity index 100% rename from src/popup/FormField/FormField.tsx rename to src/pages/FormField/FormField.tsx diff --git a/src/popup/FormField/index.ts b/src/pages/FormField/index.ts similarity index 100% rename from src/popup/FormField/index.ts rename to src/pages/FormField/index.ts diff --git a/src/pages/Options.tsx b/src/pages/Options.tsx index c6b63ba4..729648c9 100644 --- a/src/pages/Options.tsx +++ b/src/pages/Options.tsx @@ -5,10 +5,10 @@ import React, { useCallback, useEffect, useState } from "react"; import { TabNavigation, Subtitle } from "../components"; import { Features, getSettings, INITIAL_VALUES } from "../services/getSettings"; import { Settings, persistSettings, settingsSchema } from "../services"; -import { SubmitButton } from "../popup/Button"; -import { GhInstancesTab, JiraTab } from "../popup/Tabs"; -import { AutoFilterTab } from "../popup/Tabs/AutoFilterTab"; -import { FeatureTogglesTab } from "./FeatureTogglesTab"; +import { SubmitButton } from "./Button"; +import { GhInstancesTab, JiraTab } from "./Tabs"; +import { AutoFilterTab } from "./Tabs/AutoFilterTab"; +import { FeatureTogglesTab } from "./Tabs/FeatureTogglesTab/FeatureTogglesTab"; import styles from "./Options.module.scss"; export type OptionsTab = diff --git a/src/popup/Tabs/AutoFilterTab/AutoFilterTab.tsx b/src/pages/Tabs/AutoFilterTab/AutoFilterTab.tsx similarity index 100% rename from src/popup/Tabs/AutoFilterTab/AutoFilterTab.tsx rename to src/pages/Tabs/AutoFilterTab/AutoFilterTab.tsx diff --git a/src/popup/Tabs/AutoFilterTab/index.ts b/src/pages/Tabs/AutoFilterTab/index.ts similarity index 100% rename from src/popup/Tabs/AutoFilterTab/index.ts rename to src/pages/Tabs/AutoFilterTab/index.ts diff --git a/src/pages/FeatureTogglesTab.tsx b/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx similarity index 95% rename from src/pages/FeatureTogglesTab.tsx rename to src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx index 4966b07d..a6dc30ef 100644 --- a/src/pages/FeatureTogglesTab.tsx +++ b/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx @@ -5,12 +5,11 @@ import { FeatureItem, SectionTitle, Subtitle, -} from "../components"; -import { TemplateDescriptionParameters } from "../content"; -import { Features } from "../services/getSettings"; -import { ExportButton } from "./ExportButton"; -import { ImportButton } from "./ImportButton"; +} from "../../../components"; +import { TemplateDescriptionParameters } from "../../../content"; +import { Features } from "../../../services/getSettings"; import styles from "./Options.module.scss"; +import { ExportButton, ImportButton } from "../../Button"; type Props = { features: Features; diff --git a/src/pages/Tabs/FeatureTogglesTab/index.ts b/src/pages/Tabs/FeatureTogglesTab/index.ts new file mode 100644 index 00000000..4bc2c554 --- /dev/null +++ b/src/pages/Tabs/FeatureTogglesTab/index.ts @@ -0,0 +1 @@ +export { FeatureTogglesTab } from "./FeatureTogglesTab"; diff --git a/src/popup/Tabs/GhInstancesTab/GhInstancesTab.module.scss b/src/pages/Tabs/GhInstancesTab/GhInstancesTab.module.scss similarity index 100% rename from src/popup/Tabs/GhInstancesTab/GhInstancesTab.module.scss rename to src/pages/Tabs/GhInstancesTab/GhInstancesTab.module.scss diff --git a/src/popup/Tabs/GhInstancesTab/GhInstancesTab.tsx b/src/pages/Tabs/GhInstancesTab/GhInstancesTab.tsx similarity index 100% rename from src/popup/Tabs/GhInstancesTab/GhInstancesTab.tsx rename to src/pages/Tabs/GhInstancesTab/GhInstancesTab.tsx diff --git a/src/popup/Tabs/GhInstancesTab/index.ts b/src/pages/Tabs/GhInstancesTab/index.ts similarity index 100% rename from src/popup/Tabs/GhInstancesTab/index.ts rename to src/pages/Tabs/GhInstancesTab/index.ts diff --git a/src/popup/Tabs/JiraTab/JiraTab.tsx b/src/pages/Tabs/JiraTab/JiraTab.tsx similarity index 100% rename from src/popup/Tabs/JiraTab/JiraTab.tsx rename to src/pages/Tabs/JiraTab/JiraTab.tsx diff --git a/src/popup/Tabs/JiraTab/index.ts b/src/pages/Tabs/JiraTab/index.ts similarity index 100% rename from src/popup/Tabs/JiraTab/index.ts rename to src/pages/Tabs/JiraTab/index.ts diff --git a/src/popup/Tabs/index.ts b/src/pages/Tabs/index.ts similarity index 71% rename from src/popup/Tabs/index.ts rename to src/pages/Tabs/index.ts index 6ad31fcf..0a300c44 100644 --- a/src/popup/Tabs/index.ts +++ b/src/pages/Tabs/index.ts @@ -1,3 +1,4 @@ export * from "./AutoFilterTab"; +export * from "./FeatureTogglesTab"; export * from "./GhInstancesTab"; export * from "./JiraTab"; From 5ddc609e8fffa7e71124e97bb6ded8f5e0a1ea4b Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Mon, 25 Aug 2025 14:21:20 +0200 Subject: [PATCH 05/20] #54: Create FeatureTogglesTab.module.scss --- src/pages/Options.module.scss | 6 ------ .../Tabs/FeatureTogglesTab/FeatureTogglesTab.module.scss | 6 ++++++ src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.module.scss diff --git a/src/pages/Options.module.scss b/src/pages/Options.module.scss index ee565377..dfd88ee7 100644 --- a/src/pages/Options.module.scss +++ b/src/pages/Options.module.scss @@ -29,9 +29,3 @@ font-weight: bold; margin-bottom: 2px; } - -.featuresList { - display: flex; - flex-direction: column; - gap: 24px; -} diff --git a/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.module.scss b/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.module.scss new file mode 100644 index 00000000..896ecba3 --- /dev/null +++ b/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.module.scss @@ -0,0 +1,6 @@ +@import "../../../shared-component-styles.scss"; + +.featuresList { + @extend .flexColumn; + gap: 24px; +} diff --git a/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx b/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx index a6dc30ef..73bf7bc3 100644 --- a/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx +++ b/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx @@ -8,7 +8,7 @@ import { } from "../../../components"; import { TemplateDescriptionParameters } from "../../../content"; import { Features } from "../../../services/getSettings"; -import styles from "./Options.module.scss"; +import styles from "./FeatureTogglesTab.module.scss"; import { ExportButton, ImportButton } from "../../Button"; type Props = { From 0ef7e4bca8a0443070560b2c5f1859a2fc74c2f0 Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Mon, 25 Aug 2025 14:37:06 +0200 Subject: [PATCH 06/20] #54: Refactor Tab-names --- src/components/TabNavigation/TabNavigation.tsx | 17 ++++++----------- src/constants/index.ts | 1 + src/constants/tabs.ts | 12 ++++++++++++ src/pages/Options.tsx | 18 +++--------------- 4 files changed, 22 insertions(+), 26 deletions(-) create mode 100644 src/constants/index.ts create mode 100644 src/constants/tabs.ts diff --git a/src/components/TabNavigation/TabNavigation.tsx b/src/components/TabNavigation/TabNavigation.tsx index 2fa60ef4..b74df5af 100644 --- a/src/components/TabNavigation/TabNavigation.tsx +++ b/src/components/TabNavigation/TabNavigation.tsx @@ -1,20 +1,15 @@ import { UnderlineNav } from "@primer/react"; import { UnderlineNavItem } from "@primer/react/lib-esm/UnderlineNav/UnderlineNavItem"; import React from "react"; +import { Tab } from "../../constants"; -export type Tab = "GH Instances" | "Auto filter" | "Jira"; - -type Props = { - tabs: T[]; - activeTab: T; - onTabClick: (tab: T) => void; +type Props = { + tabs: readonly Tab[]; + activeTab: Tab; + onTabClick: (tab: Tab) => void; }; -export const TabNavigation = ({ - tabs, - activeTab, - onTabClick, -}: Props) => { +export const TabNavigation = ({ tabs, activeTab, onTabClick }: Props) => { return ( {tabs.map((tab) => ( diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 00000000..811d3d4a --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1 @@ +export * from "./tabs"; diff --git a/src/constants/tabs.ts b/src/constants/tabs.ts new file mode 100644 index 00000000..31f60838 --- /dev/null +++ b/src/constants/tabs.ts @@ -0,0 +1,12 @@ +export const TABS = [ + "Feature Toggles", + "GH Instances", + "Auto filter", + "Jira", +] as const; + +export type Tab = (typeof TABS)[number]; + +export const isValidTab = (tab: string): tab is Tab => { + return TABS.includes(tab as Tab); +}; diff --git a/src/pages/Options.tsx b/src/pages/Options.tsx index 729648c9..10f17b99 100644 --- a/src/pages/Options.tsx +++ b/src/pages/Options.tsx @@ -3,6 +3,7 @@ import { Banner } from "@primer/react/drafts"; import { Form, Formik, FormikHelpers } from "formik"; import React, { useCallback, useEffect, useState } from "react"; import { TabNavigation, Subtitle } from "../components"; +import { TABS, Tab } from "../constants"; import { Features, getSettings, INITIAL_VALUES } from "../services/getSettings"; import { Settings, persistSettings, settingsSchema } from "../services"; import { SubmitButton } from "./Button"; @@ -11,21 +12,8 @@ import { AutoFilterTab } from "./Tabs/AutoFilterTab"; import { FeatureTogglesTab } from "./Tabs/FeatureTogglesTab/FeatureTogglesTab"; import styles from "./Options.module.scss"; -export type OptionsTab = - | "Feature Toggles" - | "GH Instances" - | "Auto filter" - | "Jira"; - -const tabs: Array = [ - "Feature Toggles", - "GH Instances", - "Auto filter", - "Jira", -]; - export const Options = () => { - const [activeTab, setActiveTab] = useState("Feature Toggles"); + const [activeTab, setActiveTab] = useState("Feature Toggles"); const [features, setFeatures] = useState(INITIAL_VALUES.features); const [error, errorSet] = useState(); const [success, successSet] = useState(); @@ -178,7 +166,7 @@ export const Options = () => {
From 2039d484cf5aa19d1ad26302d7f33d8e65c6367d Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Mon, 25 Aug 2025 15:46:03 +0200 Subject: [PATCH 07/20] #54: Organize features by page --- .../FeatureTogglesTab/FeatureTogglesTab.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx b/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx index 73bf7bc3..59dd6c12 100644 --- a/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx +++ b/src/pages/Tabs/FeatureTogglesTab/FeatureTogglesTab.tsx @@ -31,7 +31,12 @@ export const FeatureTogglesTab: React.FC = ({ return ( <> - Enable and disable Extension-features. + + Enable and disable Extension-features. Features are organized by + GitHub-page. + + + Pull Requests List = ({ ariaLabel="Toggle auto filter" /> - onToggle("jira")} - ariaLabel="Toggle Jira integration" - /> + Individual Pull Request = ({ ariaLabel="Toggle assign random reviewer" /> + Create Pull Request + + onToggle("jira")} + ariaLabel="Toggle Jira integration" + /> + Date: Mon, 25 Aug 2025 15:51:36 +0200 Subject: [PATCH 08/20] #54: Create Export/Import tab --- src/constants/tabs.ts | 1 + src/pages/Options.tsx | 10 +++- .../FeatureTogglesTab/FeatureTogglesTab.tsx | 31 ------------- .../Tabs/ImportExportTab/ImportExportTab.tsx | 46 +++++++++++++++++++ src/pages/Tabs/ImportExportTab/index.ts | 1 + src/pages/Tabs/index.ts | 1 + 6 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 src/pages/Tabs/ImportExportTab/ImportExportTab.tsx create mode 100644 src/pages/Tabs/ImportExportTab/index.ts diff --git a/src/constants/tabs.ts b/src/constants/tabs.ts index 31f60838..b471e4f9 100644 --- a/src/constants/tabs.ts +++ b/src/constants/tabs.ts @@ -3,6 +3,7 @@ export const TABS = [ "GH Instances", "Auto filter", "Jira", + "Import/Export", ] as const; export type Tab = (typeof TABS)[number]; diff --git a/src/pages/Options.tsx b/src/pages/Options.tsx index 10f17b99..21fa5ea1 100644 --- a/src/pages/Options.tsx +++ b/src/pages/Options.tsx @@ -7,9 +7,9 @@ import { TABS, Tab } from "../constants"; import { Features, getSettings, INITIAL_VALUES } from "../services/getSettings"; import { Settings, persistSettings, settingsSchema } from "../services"; import { SubmitButton } from "./Button"; -import { GhInstancesTab, JiraTab } from "./Tabs"; +import { GhInstancesTab, ImportExportTab, JiraTab } from "./Tabs"; import { AutoFilterTab } from "./Tabs/AutoFilterTab"; -import { FeatureTogglesTab } from "./Tabs/FeatureTogglesTab/FeatureTogglesTab"; +import { FeatureTogglesTab } from "./Tabs/FeatureTogglesTab"; import styles from "./Options.module.scss"; export const Options = () => { @@ -97,6 +97,12 @@ export const Options = () => { features={features} onToggle={handleToggle} onError={showError} + /> + ); + case "Import/Export": + return ( + void; onError: (message?: string) => void; - onSuccess: (message: string) => void; - onReset: () => void; - onLoadSettings: () => void; }; export const FeatureTogglesTab: React.FC = ({ features, onToggle, onError, - onSuccess, - onReset, - onLoadSettings, }) => { return ( <> @@ -133,30 +126,6 @@ export const FeatureTogglesTab: React.FC = ({ /> )} - - Export & Import Settings - - - You can export your current settings as a JSON file. Your settings - contain access tokens. Be careful and make sure to remove your tokens - before sharing. - - - - onSuccess("Exported settings successfully")} - onClick={onReset} - /> - { - onLoadSettings(); - onSuccess("Imported settings successfully"); - }} - onClick={onReset} - /> - ); }; diff --git a/src/pages/Tabs/ImportExportTab/ImportExportTab.tsx b/src/pages/Tabs/ImportExportTab/ImportExportTab.tsx new file mode 100644 index 00000000..c61837ea --- /dev/null +++ b/src/pages/Tabs/ImportExportTab/ImportExportTab.tsx @@ -0,0 +1,46 @@ +import { Box } from "@primer/react"; +import React from "react"; +import { SectionTitle, Subtitle } from "../../../components"; +import { ExportButton, ImportButton } from "../../Button"; + +type Props = { + onError: (message?: string) => void; + onSuccess: (message: string) => void; + onReset: () => void; + onLoadSettings: () => void; +}; + +export const ImportExportTab: React.FC = ({ + onError, + onSuccess, + onReset, + onLoadSettings, +}) => { + return ( + <> + Export & Import Settings + + + You can export your current settings as a JSON file. Your settings + contain access tokens. Be careful and make sure to remove your tokens + before sharing. + + + + onSuccess("Exported settings successfully")} + onClick={onReset} + /> + { + onLoadSettings(); + onSuccess("Imported settings successfully"); + }} + onClick={onReset} + /> + + + ); +}; diff --git a/src/pages/Tabs/ImportExportTab/index.ts b/src/pages/Tabs/ImportExportTab/index.ts new file mode 100644 index 00000000..4e50ac4b --- /dev/null +++ b/src/pages/Tabs/ImportExportTab/index.ts @@ -0,0 +1 @@ +export { ImportExportTab } from "./ImportExportTab"; diff --git a/src/pages/Tabs/index.ts b/src/pages/Tabs/index.ts index 0a300c44..03cd20e3 100644 --- a/src/pages/Tabs/index.ts +++ b/src/pages/Tabs/index.ts @@ -1,4 +1,5 @@ export * from "./AutoFilterTab"; export * from "./FeatureTogglesTab"; export * from "./GhInstancesTab"; +export * from "./ImportExportTab"; export * from "./JiraTab"; From 98a00dd8558febce5c723d344187cf4244e05a3a Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Mon, 25 Aug 2025 15:53:03 +0200 Subject: [PATCH 09/20] #54: Remove title from import/export tab --- src/pages/Tabs/ImportExportTab/ImportExportTab.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/Tabs/ImportExportTab/ImportExportTab.tsx b/src/pages/Tabs/ImportExportTab/ImportExportTab.tsx index c61837ea..065b2060 100644 --- a/src/pages/Tabs/ImportExportTab/ImportExportTab.tsx +++ b/src/pages/Tabs/ImportExportTab/ImportExportTab.tsx @@ -1,6 +1,6 @@ import { Box } from "@primer/react"; import React from "react"; -import { SectionTitle, Subtitle } from "../../../components"; +import { Subtitle } from "../../../components"; import { ExportButton, ImportButton } from "../../Button"; type Props = { @@ -18,8 +18,6 @@ export const ImportExportTab: React.FC = ({ }) => { return ( <> - Export & Import Settings - You can export your current settings as a JSON file. Your settings contain access tokens. Be careful and make sure to remove your tokens From ed8d514b033580079581ee62a02ce35d72b5fe3e Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Tue, 26 Aug 2025 11:54:49 +0200 Subject: [PATCH 10/20] #54: Add CLAUDE.md --- CLAUDE.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..65ad3c10 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Development Commands + +```bash +# Development +npm run watch # Watch mode for development +npm run build # Production build +npm run build:pack # Build and create dist.zip package +npm run release # Build and package for release + +# Testing and Quality +npm test # Run Jest tests +npm run lint # Run ESLint +npm run lint:fix # Fix linting issues automatically +npm run prettier # Format code with Prettier + +# Chrome Extension Development +# Load the 'dist' directory in Chrome's extension developer mode after building +``` + +## Project Architecture + +This is a Chrome extension (Manifest v3) that enhances GitHub and GitHub Enterprise UI with productivity features. + +### Core Structure + +- **Content Scripts**: Three separate content scripts inject functionality into different GitHub pages: + - `content_pr_page.tsx` - Individual PR pages + - `content_prs_page.tsx` - PRs listing page + - `content_compare_page.tsx` - Compare/diff pages + +- **Background Service Worker**: `background.ts` handles JIRA API requests due to CORS restrictions + +- **Extension Pages**: + - `popup.tsx` - Extension popup interface + - `options.tsx` - Options/settings page with tabbed interface + +### Key Components + +- **Settings Management**: Uses Chrome storage API with Yup schema validation (`src/services/getSettings.ts`) +- **Multi-Instance Support**: Supports multiple GitHub instances (Enterprise + GitHub.com) via instance configuration +- **Feature Toggles**: All features are individually toggleable via settings +- **JIRA Integration**: Fetches issue data through background script to bypass CORS + +### Settings Architecture + +Settings are organized into: + +- `instances[]` - GitHub instance configurations (PAT, org, repo, base URL) +- `features{}` - Boolean toggles for all extension features +- `jira{}` - JIRA integration settings +- `autoFilter{}` - Custom PR filtering configuration + +### Build System + +- **Webpack**: Multi-entry build with code splitting (vendor chunk for all except background) +- **SASS Modules**: CSS modules with local scope and hash-based class names +- **TypeScript**: Full TypeScript with strict validation +- **Entry Points**: Separate bundles for popup, options, content scripts, and background + +### Chrome Extension Permissions + +- `storage` - Chrome storage for settings +- `host_permissions: [""]` - Access to all GitHub instances +- Content scripts inject into `https://*/*` patterns + +### Development Workflow + +1. Run `npm run watch` for development +2. Load `dist` directory in Chrome extension developer mode +3. Changes auto-reload with webpack watch mode +4. Use VS Code task runner (Ctrl+Shift+B) for integrated development + +## Coding Guidelines + +- **Type Definitions**: Always use `type` instead of `interface` +- **React Component Props**: Always name component props type as `Props` and never export it +- **Exports**: Never use default exports - always use named exports +- **Component Structure**: Follow existing component folder structure with index.ts barrel exports +- **Type Safety**: Never use `any` type - use proper typing with `unknown` for uncertain types +- **Type Casting**: When type casting is necessary, always include safety checks before casting + +## Important Implementation Notes + +- Settings validation uses Yup schemas with strict type checking +- GitHub API integration uses Octokit with user-provided PATs +- JIRA API calls must go through background script due to CORS +- Features are conditionally loaded based on current page URL and user settings +- CSS modules prevent style conflicts with GitHub's native styles From 553a386d9e2ee22e6ed5b5a82a1ce4c78164a759 Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Tue, 26 Aug 2025 11:55:32 +0200 Subject: [PATCH 11/20] #54: Remove isValidTab --- src/constants/tabs.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/constants/tabs.ts b/src/constants/tabs.ts index b471e4f9..e49d0e42 100644 --- a/src/constants/tabs.ts +++ b/src/constants/tabs.ts @@ -7,7 +7,3 @@ export const TABS = [ ] as const; export type Tab = (typeof TABS)[number]; - -export const isValidTab = (tab: string): tab is Tab => { - return TABS.includes(tab as Tab); -}; From 0e8fbcd03898df1aba3dd6fcefc0011b8a80293e Mon Sep 17 00:00:00 2001 From: HANS KREBS Date: Tue, 26 Aug 2025 12:10:45 +0200 Subject: [PATCH 12/20] #54: Automatically scroll FeatureInput into view when toggled --- src/components/FeatureInput/FeatureInput.tsx | 78 +++++++++---------- .../FeatureTogglesTab/FeatureTogglesTab.tsx | 13 +++- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/components/FeatureInput/FeatureInput.tsx b/src/components/FeatureInput/FeatureInput.tsx index af4b96cc..f7cc496c 100644 --- a/src/components/FeatureInput/FeatureInput.tsx +++ b/src/components/FeatureInput/FeatureInput.tsx @@ -1,5 +1,5 @@ import { Textarea } from "@primer/react"; -import React, { useEffect, useState } from "react"; +import React, { forwardRef, useEffect, useState } from "react"; import { getSettingValue, Settings } from "../../services"; type Props = { @@ -9,45 +9,45 @@ type Props = { onError: (message: string) => void; }; -export const FeatureInput: React.FC = ({ - storageKey, - onError, - ariaLabel, - placeholder, -}) => { - const [value, setValue] = useState(); +export const FeatureInput = forwardRef( + ({ storageKey, onError, ariaLabel, placeholder }, ref) => { + const [value, setValue] = useState(); - useEffect(() => { - const getValue = async () => { - const value = await getSettingValue(storageKey); - if (typeof value === "string") setValue(value); + useEffect(() => { + const getValue = async () => { + const value = await getSettingValue(storageKey); + if (typeof value === "string") setValue(value); + }; + void getValue(); + }, [storageKey]); + + const handleChange = (event: React.ChangeEvent) => { + try { + setValue(event.target.value); + chrome.storage.local.set({ [storageKey]: event.target.value }, () => { + if (chrome.runtime.lastError?.message) { + onError(chrome.runtime.lastError.message); + } + }); + } catch (error) { + onError( + error instanceof Error + ? error.message + : "An error occurred while saving the template description", + ); + } }; - void getValue(); - }, [storageKey]); - const handleChange = (event: React.ChangeEvent) => { - try { - setValue(event.target.value); - chrome.storage.local.set({ [storageKey]: event.target.value }, () => { - if (chrome.runtime.lastError?.message) { - onError(chrome.runtime.lastError.message); - } - }); - } catch (error) { - onError( - error instanceof Error - ? error.message - : "An error occurred while saving the template description", - ); - } - }; + return ( +