From 70ff4e13b9624716f137e9b235ad961e553a2816 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 24 Feb 2026 11:28:31 -0500 Subject: [PATCH] feat: Add a script for bulk GitHub issue creation and simplify the client settings profile header UI. --- .../settings_actions.dart | 37 ++-- .../settings_profile_header.dart | 33 +-- scripts/create_issues.py | 207 ++++++++++++++++++ scripts/issues-to-create.md | 27 +++ 4 files changed, 260 insertions(+), 44 deletions(-) create mode 100644 scripts/create_issues.py create mode 100644 scripts/issues-to-create.md diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart index 0e702c33..7db4d5ab 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; + import '../../blocs/client_settings_bloc.dart'; /// A widget that displays the primary actions for the settings page. @@ -27,10 +28,6 @@ class SettingsActions extends StatelessWidget { _QuickLinksCard(labels: labels), const SizedBox(height: UiConstants.space4), - // Notifications section - _NotificationsSettingsCard(), - const SizedBox(height: UiConstants.space4), - // Log Out button (outlined) BlocBuilder( builder: (BuildContext context, ClientSettingsState state) { @@ -80,15 +77,14 @@ class SettingsActions extends StatelessWidget { /// Handles the sign-out button click event. void _onSignoutClicked(BuildContext context) { - ReadContext(context) - .read() - .add(const ClientSettingsSignOutRequested()); + ReadContext( + context, + ).read().add(const ClientSettingsSignOutRequested()); } } /// Quick Links card — inline here since it's always part of SettingsActions ordering. class _QuickLinksCard extends StatelessWidget { - const _QuickLinksCard({required this.labels}); final TranslationsClientSettingsProfileEn labels; @@ -130,7 +126,6 @@ class _QuickLinksCard extends StatelessWidget { /// A single quick link row item. class _QuickLinkItem extends StatelessWidget { - const _QuickLinkItem({ required this.icon, required this.title, @@ -198,24 +193,36 @@ class _NotificationsSettingsCard extends StatelessWidget { icon: UiIcons.bell, title: context.t.client_settings.preferences.push, value: state.pushEnabled, - onChanged: (val) => ReadContext(context).read().add( - ClientSettingsNotificationToggled(type: 'push', isEnabled: val), + onChanged: (val) => + ReadContext(context).read().add( + ClientSettingsNotificationToggled( + type: 'push', + isEnabled: val, + ), ), ), _NotificationToggle( icon: UiIcons.mail, title: context.t.client_settings.preferences.email, value: state.emailEnabled, - onChanged: (val) => ReadContext(context).read().add( - ClientSettingsNotificationToggled(type: 'email', isEnabled: val), + onChanged: (val) => + ReadContext(context).read().add( + ClientSettingsNotificationToggled( + type: 'email', + isEnabled: val, + ), ), ), _NotificationToggle( icon: UiIcons.phone, title: context.t.client_settings.preferences.sms, value: state.smsEnabled, - onChanged: (val) => ReadContext(context).read().add( - ClientSettingsNotificationToggled(type: 'sms', isEnabled: val), + onChanged: (val) => + ReadContext(context).read().add( + ClientSettingsNotificationToggled( + type: 'sms', + isEnabled: val, + ), ), ), ], diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart index 61dbf227..c6987214 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart @@ -12,7 +12,8 @@ class SettingsProfileHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final TranslationsClientSettingsProfileEn labels = t.client_settings.profile; + final TranslationsClientSettingsProfileEn labels = + t.client_settings.profile; final dc.ClientSession? session = dc.ClientSessionStore.instance.session; final String businessName = session?.business?.businessName ?? 'Your Company'; @@ -26,9 +27,7 @@ class SettingsProfileHeader extends StatelessWidget { child: Container( width: double.infinity, padding: const EdgeInsets.only(bottom: 36), - decoration: const BoxDecoration( - color: UiColors.primary, - ), + decoration: const BoxDecoration(color: UiColors.primary), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -75,13 +74,6 @@ class SettingsProfileHeader extends StatelessWidget { color: UiColors.white.withValues(alpha: 0.6), width: 3, ), - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.15), - blurRadius: 16, - offset: const Offset(0, 6), - ), - ], ), child: ClipOval( child: photoUrl != null && photoUrl.isNotEmpty @@ -103,9 +95,7 @@ class SettingsProfileHeader extends StatelessWidget { // ── Business Name ───────────────────────────────── Text( businessName, - style: UiTypography.headline3m.copyWith( - color: UiColors.white, - ), + style: UiTypography.headline3m.copyWith(color: UiColors.white), ), const SizedBox(height: UiConstants.space2), @@ -128,21 +118,6 @@ class SettingsProfileHeader extends StatelessWidget { ), ], ), - const SizedBox(height: UiConstants.space5), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 100), - child: UiButton.secondary( - text: labels.edit_profile, - size: UiButtonSize.small, - onPressed: () => - Modular.to.pushNamed('${ClientPaths.settings}/edit-profile'), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.white, - side: const BorderSide(color: UiColors.white, width: 1.5), - backgroundColor: UiColors.white.withValues(alpha: 0.1), - ), - ), - ), ], ), ), diff --git a/scripts/create_issues.py b/scripts/create_issues.py new file mode 100644 index 00000000..bbe0b071 --- /dev/null +++ b/scripts/create_issues.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +import subprocess +import os +import re +import argparse + +# --- Configuration --- +INPUT_FILE = "issues-to-create.md" +DEFAULT_PROJECT_TITLE = None +DEFAULT_MILESTONE = "Milestone 4" +# --- + +def parse_issues(content): + """Parse issue blocks from markdown content. + + Each issue block starts with a '# Title' line, followed by an optional + 'Labels:' metadata line, then the body. Milestone is set globally, not per-issue. + """ + issue_blocks = re.split(r'\n(?=#\s)', content) + issues = [] + + for block in issue_blocks: + if not block.strip(): + continue + + lines = block.strip().split('\n') + + # Title: strip leading '#' characters and whitespace + title = re.sub(r'^#+\s*', '', lines[0]).strip() + + labels_line = "" + body_start_index = len(lines) # default: no body + + # Only 'Labels:' is parsed from the markdown; milestone is global + for i, line in enumerate(lines[1:], start=1): + stripped = line.strip() + if stripped.lower().startswith('labels:'): + labels_line = stripped.split(':', 1)[1].strip() + elif stripped == "": + continue # skip blank separator lines in the header + else: + body_start_index = i + break + + body = "\n".join(lines[body_start_index:]).strip() + labels = [label.strip() for label in labels_line.split(',') if label.strip()] + + if not title: + print("⚠️ Skipping block with no title.") + continue + + issues.append({ + "title": title, + "body": body, + "labels": labels, + }) + + return issues + + +def main(): + parser = argparse.ArgumentParser( + description="Bulk create GitHub issues from a markdown file.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Input file format (issues-to-create.md): +----------------------------------------- +# Issue Title One +Labels: bug, enhancement + +This is the body of the first issue. +It can span multiple lines. + +# Issue Title Two +Labels: documentation + +Body of the second issue. +----------------------------------------- +All issues share the same project and milestone, configured at the top of this script +or passed via --project and --milestone flags. + """ + ) + parser.add_argument( + "--file", "-f", + default=INPUT_FILE, + help=f"Path to the markdown input file (default: {INPUT_FILE})" + ) + parser.add_argument( + "--project", "-p", + default=DEFAULT_PROJECT_TITLE, + help=f"GitHub Project title for all issues (default: {DEFAULT_PROJECT_TITLE})" + ) + parser.add_argument( + "--milestone", "-m", + default=DEFAULT_MILESTONE, + help=f"Milestone to assign to all issues (default: {DEFAULT_MILESTONE})" + ) + parser.add_argument( + "--no-project", + action="store_true", + help="Do not add issues to any project." + ) + parser.add_argument( + "--no-milestone", + action="store_true", + help="Do not assign a milestone to any issue." + ) + parser.add_argument( + "--repo", "-r", + default=None, + help="Target GitHub repo in OWNER/REPO format (uses gh default if not set)." + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Parse the file and print issues without creating them." + ) + args = parser.parse_args() + + input_file = args.file + project_title = args.project if not args.no_project else None + milestone = args.milestone if not args.no_milestone else None + + print("🚀 Bulk GitHub Issue Creator") + print("=" * 40) + print(f" Input file: {input_file}") + print(f" Project: {project_title or '(none)'}") + print(f" Milestone: {milestone or '(none)'}") + if args.repo: + print(f" Repo: {args.repo}") + if args.dry_run: + print(" Mode: DRY RUN (no issues will be created)") + print("=" * 40) + + # --- Preflight checks --- + if subprocess.run(["which", "gh"], capture_output=True).returncode != 0: + print("❌ ERROR: GitHub CLI ('gh') is not installed or not in PATH.") + print(" Install it from: https://cli.github.com/") + exit(1) + + if not os.path.exists(input_file): + print(f"❌ ERROR: Input file '{input_file}' not found.") + exit(1) + + print("✅ Preflight checks passed.\n") + + # --- Parse --- + print(f"📄 Parsing '{input_file}'...") + with open(input_file, 'r') as f: + content = f.read() + + issues = parse_issues(content) + + if not issues: + print("⚠️ No issues found in the input file. Check the format.") + exit(0) + + print(f" Found {len(issues)} issue(s) to create.\n") + + # --- Create --- + success_count = 0 + fail_count = 0 + + for idx, issue in enumerate(issues, start=1): + print(f"[{idx}/{len(issues)}] {issue['title']}") + if issue['labels']: + print(f" Labels: {', '.join(issue['labels'])}") + print(f" Milestone: {milestone or '(none)'}") + print(f" Project: {project_title or '(none)'}") + + if args.dry_run: + print(" (dry-run — skipping creation)\n") + continue + + command = ["gh", "issue", "create"] + if args.repo: + command.extend(["--repo", args.repo]) + command.extend(["--title", issue["title"]]) + command.extend(["--body", issue["body"] or " "]) # gh requires non-empty body + + if project_title: + command.extend(["--project", project_title]) + if milestone: + command.extend(["--milestone", milestone]) + for label in issue["labels"]: + command.extend(["--label", label]) + + try: + result = subprocess.run(command, check=True, text=True, capture_output=True) + print(f" ✅ Created: {result.stdout.strip()}") + success_count += 1 + except subprocess.CalledProcessError as e: + print(f" ❌ Failed: {e.stderr.strip()}") + fail_count += 1 + + print() + + # --- Summary --- + print("=" * 40) + if args.dry_run: + print(f"🔍 Dry run complete. {len(issues)} issue(s) parsed, none created.") + else: + print(f"🎉 Done! {success_count} created, {fail_count} failed.") + + +if __name__ == "__main__": + main() diff --git a/scripts/issues-to-create.md b/scripts/issues-to-create.md new file mode 100644 index 00000000..8172f5bf --- /dev/null +++ b/scripts/issues-to-create.md @@ -0,0 +1,27 @@ +# +Labels: + + + +## Scope + +### +- + +## +- [ ] + +------- + +# +Labels: + + + +## Scope + +### +- + +## +- [ ]