From 73aed2d4356cc81ede44939b5ffb068a78170914 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 8 Oct 2025 16:07:39 +0530 Subject: [PATCH 01/36] feat: merged filter-sync-issue branch as squash --- backend/Makefile | 2 +- backend/apps/github/admin/issue.py | 1 + .../github/migrations/0037_issue_level.py | 26 ++ backend/apps/github/models/issue.py | 8 + backend/apps/mentorship/Makefile | 5 + backend/apps/mentorship/admin/module.py | 1 + backend/apps/mentorship/admin/task.py | 2 + .../apps/mentorship/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/sync_issue_levels.py | 97 +++++++ .../management/commands/sync_module_issues.py | 237 ++++++++++++++++++ ...emove_task_level_module_issues_and_more.py | 37 +++ backend/apps/mentorship/models/module.py | 8 + backend/apps/mentorship/models/task.py | 12 +- backend/apps/mentorship/utils.py | 6 + 15 files changed, 431 insertions(+), 11 deletions(-) create mode 100644 backend/apps/github/migrations/0037_issue_level.py create mode 100644 backend/apps/mentorship/Makefile create mode 100644 backend/apps/mentorship/management/__init__.py create mode 100644 backend/apps/mentorship/management/commands/__init__.py create mode 100644 backend/apps/mentorship/management/commands/sync_issue_levels.py create mode 100644 backend/apps/mentorship/management/commands/sync_module_issues.py create mode 100644 backend/apps/mentorship/migrations/0005_remove_task_level_module_issues_and_more.py create mode 100644 backend/apps/mentorship/utils.py diff --git a/backend/Makefile b/backend/Makefile index 2b2d68c877..e2acb4ec1e 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,6 +1,6 @@ include backend/apps/ai/Makefile include backend/apps/github/Makefile -include backend/apps/nest/Makefile +include backend/apps/mentorship/Makefile include backend/apps/owasp/Makefile include backend/apps/slack/Makefile diff --git a/backend/apps/github/admin/issue.py b/backend/apps/github/admin/issue.py index 787781ffec..3c397dcd85 100644 --- a/backend/apps/github/admin/issue.py +++ b/backend/apps/github/admin/issue.py @@ -19,6 +19,7 @@ class IssueAdmin(admin.ModelAdmin): "repository", "created_at", "title", + "level", "custom_field_github_url", ) list_filter = ( diff --git a/backend/apps/github/migrations/0037_issue_level.py b/backend/apps/github/migrations/0037_issue_level.py new file mode 100644 index 0000000000..de309aade5 --- /dev/null +++ b/backend/apps/github/migrations/0037_issue_level.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.5 on 2025-09-25 08:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0036_user_has_public_member_page_alter_organization_name_and_more"), + ("mentorship", "0004_module_key_program_key_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="issue", + name="level", + field=models.ForeignKey( + blank=True, + help_text="The difficulty level of this issue.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issues", + to="mentorship.tasklevel", + ), + ), + ] diff --git a/backend/apps/github/models/issue.py b/backend/apps/github/models/issue.py index 693272a017..97a08ceae3 100644 --- a/backend/apps/github/models/issue.py +++ b/backend/apps/github/models/issue.py @@ -54,6 +54,14 @@ class Meta: null=True, related_name="created_issues", ) + level = models.ForeignKey( + "mentorship.TaskLevel", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="issues", + help_text="The difficulty level of this issue.", + ) milestone = models.ForeignKey( "github.Milestone", on_delete=models.CASCADE, diff --git a/backend/apps/mentorship/Makefile b/backend/apps/mentorship/Makefile new file mode 100644 index 0000000000..a43f999761 --- /dev/null +++ b/backend/apps/mentorship/Makefile @@ -0,0 +1,5 @@ +mentorship-sync-module-issues: + @CMD="python manage.py sync_module_issues" $(MAKE) exec-backend-command + +mentorship-sync-issue-levels: + @CMD="python manage.py sync_issue_levels" $(MAKE) exec-backend-command diff --git a/backend/apps/mentorship/admin/module.py b/backend/apps/mentorship/admin/module.py index 1eb248c4dc..df25f85a93 100644 --- a/backend/apps/mentorship/admin/module.py +++ b/backend/apps/mentorship/admin/module.py @@ -13,6 +13,7 @@ class ModuleAdmin(admin.ModelAdmin): "program", "project", ) + autocomplete_fields = ("issues",) search_fields = ( "name", diff --git a/backend/apps/mentorship/admin/task.py b/backend/apps/mentorship/admin/task.py index 74662c10f1..e5ba082a8f 100644 --- a/backend/apps/mentorship/admin/task.py +++ b/backend/apps/mentorship/admin/task.py @@ -25,5 +25,7 @@ class TaskAdmin(admin.ModelAdmin): list_filter = ("status", "module") + ordering = ["-assigned_at"] + admin.site.register(Task, TaskAdmin) diff --git a/backend/apps/mentorship/management/__init__.py b/backend/apps/mentorship/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/apps/mentorship/management/commands/__init__.py b/backend/apps/mentorship/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/apps/mentorship/management/commands/sync_issue_levels.py b/backend/apps/mentorship/management/commands/sync_issue_levels.py new file mode 100644 index 0000000000..a1abc1da38 --- /dev/null +++ b/backend/apps/mentorship/management/commands/sync_issue_levels.py @@ -0,0 +1,97 @@ +"""A command to sync issue level with Tasklevel.""" + +from django.core.management.base import BaseCommand +from django.db.models import Prefetch + +from apps.github.models.issue import Issue +from apps.github.models.label import Label +from apps.mentorship.models.task_level import TaskLevel +from apps.mentorship.utils import normalize_name + + +class Command(BaseCommand): + """Syncs the `level` field on Issues based on matching labels, respecting Module constraints. + + If any label matches a TaskLevel in the Issue's Module, that TaskLevel is assigned. + """ + + help = "Assigns a TaskLevel to each Issue by matching labels within the same Module." + + def _build_module_level_maps(self, all_levels): + """Build a mapping from module ID to a dictionary of data. + + The dictionary contains a 'label_to_level_map' for normalized label/level + names to TaskLevel objects. + """ + module_data_map = {} + for level in all_levels: + module_id = level.module_id + level_map_container = module_data_map.setdefault(module_id, {"label_to_level_map": {}}) + level_map = level_map_container["label_to_level_map"] + + normalized_level_name = normalize_name(level.name) + level_map[normalized_level_name] = level + + for label_name in level.labels: + normalized_label = normalize_name(label_name) + level_map[normalized_label] = level + return module_data_map + + def _find_best_match_level( + self, + issue_labels_normalized, + issue_mentorship_modules, + module_data_map, + ): + """Find the best matching TaskLevel for an issue based on its labels and modules.""" + for module in issue_mentorship_modules: + if module.id in module_data_map: + module_level_map = module_data_map[module.id]["label_to_level_map"] + for label_name in issue_labels_normalized: + if label_name in module_level_map: + return module_level_map[label_name] + return None + + def handle(self, *args, **options): + self.stdout.write("Starting...") + + # 1. Build a per-module map (normalized label → TaskLevel) + all_levels = TaskLevel.objects.select_related("module").order_by("name") + + if not all_levels.exists(): + self.stdout.write( + self.style.WARNING("No TaskLevel objects found in the database. Exiting.") + ) + return + + module_data_map = self._build_module_level_maps(all_levels) + self.stdout.write(f"Built label maps for {len(module_data_map)} modules.") + + # 2.match issue labels to TaskLevels + issues_to_update = [] + issues_query = Issue.objects.prefetch_related( + Prefetch("labels", queryset=Label.objects.only("name")), + "mentorship_modules", + ).select_related("level") + + for issue in issues_query: + issue_labels_normalized = {normalize_name(label.name) for label in issue.labels.all()} + + best_match_level = self._find_best_match_level( + issue_labels_normalized, + list(issue.mentorship_modules.all()), + module_data_map, + ) + + if issue.level != best_match_level: + issue.level = best_match_level + issues_to_update.append(issue) + + if issues_to_update: + updated_count = len(issues_to_update) + Issue.objects.bulk_update(issues_to_update, ["level"]) + self.stdout.write( + self.style.SUCCESS(f"Successfully updated the level for {updated_count} issues.") + ) + else: + self.stdout.write(self.style.SUCCESS("All issue levels are already up-to-date.")) diff --git a/backend/apps/mentorship/management/commands/sync_module_issues.py b/backend/apps/mentorship/management/commands/sync_module_issues.py new file mode 100644 index 0000000000..9a19fd6b6f --- /dev/null +++ b/backend/apps/mentorship/management/commands/sync_module_issues.py @@ -0,0 +1,237 @@ +"""A command to sync update relation between module and issue and create task.""" + +from urllib.parse import urlparse + +from django.core.management.base import BaseCommand +from django.db import transaction +from django.utils import timezone +from github.GithubException import GithubException + +from apps.github.auth import get_github_client +from apps.github.models.issue import Issue +from apps.mentorship.models.module import Module +from apps.mentorship.models.task import Task +from apps.mentorship.utils import normalize_name + + +class Command(BaseCommand): + """Efficiently syncs issues to mentorship modules based on matching labels.""" + + help = ( + "Syncs issues to modules by matching labels from all repositories " + "associated with the module's project and creates related tasks." + ) + ALLOWED_GITHUB_HOSTS = {"github.com", "www.github.com"} + REPO_PATH_PARTS = 2 + + def _extract_repo_full_name(self, repository): + """Extract repository full name from Repository model or URL string.""" + if hasattr(repository, "path"): + return repository.path + + repo_url = str(repository) if repository else "" + parsed = urlparse(repo_url) + if parsed.netloc in self.ALLOWED_GITHUB_HOSTS: + parts = parsed.path.strip("/").split("/") + if len(parts) >= self.REPO_PATH_PARTS: + return "/".join(parts[: self.REPO_PATH_PARTS]) + return None + return None + + def _get_status(self, issue, assignee): + """Map GitHub issue state + assignment to task status.""" + if issue.state.lower() == "closed": + return Task.Status.COMPLETED + + if assignee: + return Task.Status.IN_PROGRESS + + return Task.Status.TODO + + def _get_last_assigned_date(self, repo, issue_number, assignee_login): + """Find the most recent 'assigned' event for a specific user using PyGithub.""" + try: + gh_issue = repo.get_issue(number=issue_number) + last_dt = None + for event in gh_issue.get_events(): + if ( + event.event == "assigned" + and event.assignee + and event.assignee.login == assignee_login + ): + last_dt = event.created_at + + if last_dt and timezone.is_naive(last_dt): + return timezone.make_aware(last_dt, timezone.utc) + return last_dt # noqa: TRY300 + + except GithubException as e: + self.stderr.write( + self.style.ERROR(f"Unexpected error for {repo.name}#{issue_number}: {e}") + ) + + return None + + def _build_repo_label_to_issue_map(self): + """Build a map from (repository_id, normalized_label_name) to a set of issue IDs.""" + self.stdout.write("Building a repository-aware map of labels to issues...") + repo_label_to_issue_ids = {} + rows = ( + Issue.objects.filter(labels__isnull=False, repository__isnull=False) + .values_list("id", "repository_id", "labels__name") + .iterator(chunk_size=5000) + ) + for issue_id, repo_id, label_name in rows: + key = (repo_id, normalize_name(label_name)) + repo_label_to_issue_ids.setdefault(key, set()).add(issue_id) + + self.stdout.write( + f"Map built. Found issues for {len(repo_label_to_issue_ids)} unique repo-label pairs." + ) + return repo_label_to_issue_ids + + def _process_module( + self, + module, + repo_label_to_issue_ids, + gh_client, + repo_cache, + verbosity, + ): + """Process a single module to link issues and create tasks.""" + project_repos = list(module.project.repositories.all()) + linked_label_names = module.labels + num_tasks_created = 0 + + matched_issue_ids = set() + for repo in project_repos: + for label_name in linked_label_names: + normalized_label = normalize_name(label_name) + key = (repo.id, normalized_label) + issues_for_label = repo_label_to_issue_ids.get(key, set()) + matched_issue_ids.update(issues_for_label) + + with transaction.atomic(): + module.issues.set(matched_issue_ids) + + if matched_issue_ids: + issues = ( + Issue.objects.filter( + id__in=matched_issue_ids, + assignees__isnull=False, + ) + .select_related("repository") + .prefetch_related("assignees", "labels") + .distinct() + ) + + for issue in issues: + assignee = issue.assignees.first() + if not assignee: + continue + + status = self._get_status(issue, assignee) + task, created = Task.objects.get_or_create( + issue=issue, + assignee=assignee, + defaults={"module": module, "status": status}, + ) + + updates = {} + if task.module != module: + updates["module"] = module + if task.status != status: + updates["status"] = status + + # Only fetch assigned_at when needed. + if (created or task.assigned_at is None) and issue.repository: + repo_full_name = self._extract_repo_full_name(issue.repository) + if repo_full_name: + if repo_full_name not in repo_cache: + try: + repo_cache[repo_full_name] = gh_client.get_repo(repo_full_name) + except GithubException as e: + self.stderr.write( + self.style.ERROR( + f"Failed to fetch repo '{repo_full_name}': {e}" + ) + ) + repo_cache[repo_full_name] = None + repo = repo_cache.get(repo_full_name) + if repo: + assigned_date = self._get_last_assigned_date( + repo=repo, + issue_number=issue.number, + assignee_login=assignee.login, + ) + if assigned_date: + updates["assigned_at"] = assigned_date + + if created: + num_tasks_created += 1 + self.stdout.write( + self.style.SUCCESS( + f"Task created for user '{assignee.login}' on issue " + f"{issue.repository.name}#{issue.number} " + f"in module '{module.name}'" + ) + ) + + if updates: + for field, value in updates.items(): + setattr(task, field, value) + task.save(update_fields=list(updates.keys())) + + num_linked = len(matched_issue_ids) + if num_linked > 0: + repo_names = ", ".join([r.name for r in project_repos]) + log_message = ( + f"Updated module '{module.name}': set {num_linked} issues from " + f"repos: [{repo_names}]" + ) + if num_tasks_created > 0: + log_message += f" and created {num_tasks_created} tasks." + + self.stdout.write(self.style.SUCCESS(log_message)) + + if verbosity > 1 and num_tasks_created > 0: + self.stdout.write(self.style.SUCCESS(f" - Created {num_tasks_created} tasks.")) + return num_linked + + def handle(self, *_args, **options): + self.stdout.write("starting...") + verbosity = options["verbosity"] + gh_client = get_github_client() + repo_cache = {} + + repo_label_to_issue_ids = self._build_repo_label_to_issue_map() + + total_links_created = 0 + total_modules_updated = 0 + + self.stdout.write("Processing modules and linking issues...") + modules_to_process = ( + Module.objects.prefetch_related("project__repositories") + .exclude(project__repositories__isnull=True) + .exclude(labels__isnull=True) + .exclude(labels=[]) + ) + + for module in modules_to_process: + links_created = self._process_module( + module=module, + repo_label_to_issue_ids=repo_label_to_issue_ids, + gh_client=gh_client, + repo_cache=repo_cache, + verbosity=verbosity, + ) + if links_created > 0: + total_links_created += links_created + total_modules_updated += 1 + + self.stdout.write( + self.style.SUCCESS( + f"Completed. {total_links_created} issue links set " + f"across {total_modules_updated} modules." + ) + ) diff --git a/backend/apps/mentorship/migrations/0005_remove_task_level_module_issues_and_more.py b/backend/apps/mentorship/migrations/0005_remove_task_level_module_issues_and_more.py new file mode 100644 index 0000000000..92fd8e1acf --- /dev/null +++ b/backend/apps/mentorship/migrations/0005_remove_task_level_module_issues_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.5 on 2025-09-25 08:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0037_issue_level"), + ("mentorship", "0004_module_key_program_key_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="task", + name="level", + ), + migrations.AddField( + model_name="module", + name="issues", + field=models.ManyToManyField( + blank=True, + help_text="Issues linked to this module via label matching.", + related_name="mentorship_modules", + to="github.issue", + verbose_name="Linked Issues", + ), + ), + migrations.AlterField( + model_name="task", + name="assigned_at", + field=models.DateTimeField( + blank=True, + help_text="Timestamp when the task was assigned to the mentee.", + null=True, + ), + ), + ] diff --git a/backend/apps/mentorship/models/module.py b/backend/apps/mentorship/models/module.py index 7309baca73..c9e13d2287 100644 --- a/backend/apps/mentorship/models/module.py +++ b/backend/apps/mentorship/models/module.py @@ -65,6 +65,14 @@ class Meta: ) # M2Ms. + issues = models.ManyToManyField( + "github.Issue", + verbose_name="Linked Issues", + related_name="mentorship_modules", + blank=True, + help_text="Issues linked to this module via label matching.", + ) + mentors = models.ManyToManyField( "mentorship.Mentor", verbose_name="Mentors", diff --git a/backend/apps/mentorship/models/task.py b/backend/apps/mentorship/models/task.py index a572b76bde..4160865310 100644 --- a/backend/apps/mentorship/models/task.py +++ b/backend/apps/mentorship/models/task.py @@ -25,7 +25,8 @@ class Status(models.TextChoices): COMPLETED = "COMPLETED", "Completed" assigned_at = models.DateTimeField( - auto_now_add=True, + null=True, + blank=True, help_text="Timestamp when the task was assigned to the mentee.", ) @@ -63,15 +64,6 @@ class Status(models.TextChoices): help_text="The GitHub issue this task corresponds to.", ) - level = models.ForeignKey( - "mentorship.TaskLevel", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="tasks", - help_text="The difficulty level of this task.", - ) - module = models.ForeignKey( "mentorship.Module", on_delete=models.CASCADE, diff --git a/backend/apps/mentorship/utils.py b/backend/apps/mentorship/utils.py new file mode 100644 index 0000000000..8432cb97c2 --- /dev/null +++ b/backend/apps/mentorship/utils.py @@ -0,0 +1,6 @@ +"""Utility functions for the mentorship app.""" + + +def normalize_name(name): + """Normalize a string by stripping whitespace and converting to lowercase.""" + return (name or "").strip().casefold() From 2b03b653318651f363c873c8678d262a326edce8 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 8 Oct 2025 16:12:33 +0530 Subject: [PATCH 02/36] feat: merged contributor-interested branch and resolved conflicts --- backend/Makefile | 1 + backend/apps/github/admin/__init__.py | 1 + backend/apps/github/admin/comment.py | 21 +++ backend/apps/github/common.py | 70 ++++++++ .../apps/github/migrations/0036_comment.py | 61 +++++++ backend/apps/github/models/__init__.py | 1 + backend/apps/github/models/comment.py | 85 ++++++++++ backend/apps/github/models/issue.py | 14 ++ backend/apps/mentorship/Makefile | 4 + backend/apps/mentorship/admin/__init__.py | 1 + .../mentorship/admin/issue_user_interest.py | 16 ++ .../commands/mentorship_update_comments.py | 152 ++++++++++++++++++ .../migrations/0005_issueuserinterest.py | 55 +++++++ backend/apps/mentorship/models/__init__.py | 1 + .../mentorship/models/issue_user_interest.py | 31 ++++ .../mentorship/models/managers/__init__.py | 1 + .../apps/mentorship/models/managers/module.py | 13 ++ backend/apps/mentorship/models/module.py | 4 + 18 files changed, 532 insertions(+) create mode 100644 backend/apps/github/admin/comment.py create mode 100644 backend/apps/github/migrations/0036_comment.py create mode 100644 backend/apps/github/models/comment.py create mode 100644 backend/apps/mentorship/admin/issue_user_interest.py create mode 100644 backend/apps/mentorship/management/commands/mentorship_update_comments.py create mode 100644 backend/apps/mentorship/migrations/0005_issueuserinterest.py create mode 100644 backend/apps/mentorship/models/issue_user_interest.py create mode 100644 backend/apps/mentorship/models/managers/__init__.py create mode 100644 backend/apps/mentorship/models/managers/module.py diff --git a/backend/Makefile b/backend/Makefile index e2acb4ec1e..1ce213c1ef 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,6 +1,7 @@ include backend/apps/ai/Makefile include backend/apps/github/Makefile include backend/apps/mentorship/Makefile +include backend/apps/nest/Makefile include backend/apps/owasp/Makefile include backend/apps/slack/Makefile diff --git a/backend/apps/github/admin/__init__.py b/backend/apps/github/admin/__init__.py index df3c242bed..090dbe6a8e 100644 --- a/backend/apps/github/admin/__init__.py +++ b/backend/apps/github/admin/__init__.py @@ -1,5 +1,6 @@ """Github app admin.""" +from .comment import CommentAdmin from .issue import IssueAdmin from .label import LabelAdmin from .milestone import MilestoneAdmin diff --git a/backend/apps/github/admin/comment.py b/backend/apps/github/admin/comment.py new file mode 100644 index 0000000000..c15455acae --- /dev/null +++ b/backend/apps/github/admin/comment.py @@ -0,0 +1,21 @@ +"""GitHub app Comment model admin.""" + +from django.contrib import admin + +from apps.github.models import Comment + + +class CommentAdmin(admin.ModelAdmin): + """Admin for Comment model.""" + + list_display = ( + "body", + "author", + "nest_created_at", + "nest_updated_at", + ) + list_filter = ("nest_created_at", "nest_updated_at") + search_fields = ("body", "author__login") + + +admin.site.register(Comment, CommentAdmin) diff --git a/backend/apps/github/common.py b/backend/apps/github/common.py index 7281963983..a2bfa79097 100644 --- a/backend/apps/github/common.py +++ b/backend/apps/github/common.py @@ -4,10 +4,15 @@ import logging from datetime import timedelta as td +from typing import TYPE_CHECKING from django.utils import timezone from github.GithubException import UnknownObjectException +if TYPE_CHECKING: + from github import Github + +from apps.github.models.comment import Comment from apps.github.models.issue import Issue from apps.github.models.label import Label from apps.github.models.milestone import Milestone @@ -227,3 +232,68 @@ def sync_repository( ) return organization, repository + + +def sync_issue_comments(gh_client: Github, issue: Issue): + """Sync new comments for a mentorship program specific issue on-demand. + + Args: + gh_client (Github): GitHub client. + issue (Issue): The local database Issue object to sync comments for. + + """ + logger.info("Starting comment sync for issue #%s", issue.number) + + try: + if not (repository := issue.repository): + logger.warning("Issue #%s has no repository, skipping", issue.number) + return + + logger.info("Fetching repository: %s", repository.path) + + gh_repository = gh_client.get_repo(repository.path) + gh_issue = gh_repository.get_issue(number=issue.number) + + since = ( + (issue.latest_comment.updated_at or issue.latest_comment.created_at) + if issue.latest_comment + else getattr(issue, "updated_at", None) + ) + + comments = [] + + gh_comments = gh_issue.get_comments(since=since) if since else gh_issue.get_comments() + + for gh_comment in gh_comments: + author = User.update_data(gh_comment.user) + if not author: + logger.warning("Could not sync author for comment %s", gh_comment.id) + continue + + comment = Comment.update_data( + gh_comment, + author=author, + content_object=issue, + save=False, + ) + comments.append(comment) + + if comments: + Comment.bulk_save(comments) + logger.info( + "%d comments synced for issue #%s", + len(comments), + issue.number, + ) + + except UnknownObjectException as e: + logger.warning( + "Could not access issue #%s. Error: %s", + issue.number, + e, + ) + except Exception: + logger.exception( + "An unexpected error occurred during comment sync for issue #%s", + issue.number, + ) diff --git a/backend/apps/github/migrations/0036_comment.py b/backend/apps/github/migrations/0036_comment.py new file mode 100644 index 0000000000..2c67ebd898 --- /dev/null +++ b/backend/apps/github/migrations/0036_comment.py @@ -0,0 +1,61 @@ +# Generated by Django 5.2.5 on 2025-09-24 13:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("github", "0035_alter_user_bio_alter_user_is_owasp_staff"), + ] + + operations = [ + migrations.CreateModel( + name="Comment", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("nest_created_at", models.DateTimeField(auto_now_add=True)), + ("nest_updated_at", models.DateTimeField(auto_now=True)), + ("github_id", models.BigIntegerField(unique=True, verbose_name="Github ID")), + ( + "created_at", + models.DateTimeField(blank=True, null=True, verbose_name="Created at"), + ), + ( + "updated_at", + models.DateTimeField( + blank=True, db_index=True, null=True, verbose_name="Updated at" + ), + ), + ("body", models.TextField(verbose_name="Body")), + ("object_id", models.PositiveIntegerField()), + ( + "author", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="comments", + to="github.user", + ), + ), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype" + ), + ), + ], + options={ + "verbose_name": "Comment", + "verbose_name_plural": "Comments", + "db_table": "github_comments", + "ordering": ("-nest_created_at",), + }, + ), + ] diff --git a/backend/apps/github/models/__init__.py b/backend/apps/github/models/__init__.py index 094a7b9900..c604c01013 100644 --- a/backend/apps/github/models/__init__.py +++ b/backend/apps/github/models/__init__.py @@ -1,5 +1,6 @@ """Github app.""" +from .comment import Comment from .milestone import Milestone from .pull_request import PullRequest from .user import User diff --git a/backend/apps/github/models/comment.py b/backend/apps/github/models/comment.py new file mode 100644 index 0000000000..3543dfd085 --- /dev/null +++ b/backend/apps/github/models/comment.py @@ -0,0 +1,85 @@ +"""GitHub app comment model.""" + +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from apps.common.models import BulkSaveModel, TimestampedModel +from apps.common.utils import truncate + + +class Comment(BulkSaveModel, TimestampedModel): + """Represents a comment on a GitHub Issue.""" + + class Meta: + db_table = "github_comments" + verbose_name = "Comment" + verbose_name_plural = "Comments" + ordering = ("-nest_created_at",) + + github_id = models.BigIntegerField(unique=True, verbose_name="Github ID") + created_at = models.DateTimeField(verbose_name="Created at", null=True, blank=True) + updated_at = models.DateTimeField( + verbose_name="Updated at", null=True, blank=True, db_index=True + ) + author = models.ForeignKey( + "github.User", on_delete=models.SET_NULL, null=True, related_name="comments" + ) + body = models.TextField(verbose_name="Body") + + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey("content_type", "object_id") + + def __str__(self): + """Return a string representation of the comment.""" + return f"{self.author} - {truncate(self.body, 50)}" + + def from_github(self, gh_comment, author=None): + """Populate fields from a GitHub API comment object.""" + field_mapping = { + "body": "body", + "created_at": "created_at", + "updated_at": "updated_at", + } + + for model_field, gh_field in field_mapping.items(): + value = getattr(gh_comment, gh_field, None) + if value is not None: + setattr(self, model_field, value) + + self.author = author + + @staticmethod + def bulk_save(comments, fields=None): # type: ignore[override] + """Bulk save comments.""" + BulkSaveModel.bulk_save(Comment, comments, fields=fields) + + @staticmethod + def update_data(gh_comment, *, author=None, content_object=None, save: bool = True): + """Update or create a Comment instance from a GitHub comment object. + + Args: + gh_comment (github.IssueComment.IssueComment): GitHub comment object. + author (User, optional): Comment author. Defaults to None. + content_object (GenericForeignKey, optional): Content object. Defaults to None. + save (bool, optional): Whether to save the instance immediately. Defaults to True. + + Returns: + Comment: The updated or newly created Comment instance. + + """ + try: + comment = Comment.objects.get(github_id=gh_comment.id) + except Comment.DoesNotExist: + comment = Comment(github_id=gh_comment.id) + + comment.from_github(gh_comment, author=author) + + if content_object is not None: + comment.content_object = content_object + + if save: + comment.save() + + return comment diff --git a/backend/apps/github/models/issue.py b/backend/apps/github/models/issue.py index 97a08ceae3..15abe3f971 100644 --- a/backend/apps/github/models/issue.py +++ b/backend/apps/github/models/issue.py @@ -4,6 +4,7 @@ from functools import lru_cache +from django.contrib.contenttypes.fields import GenericRelation from django.db import models from apps.common.index import IndexBase @@ -62,6 +63,9 @@ class Meta: related_name="issues", help_text="The difficulty level of this issue.", ) + + comments = GenericRelation("github.Comment", related_query_name="issue") + milestone = models.ForeignKey( "github.Milestone", on_delete=models.CASCADE, @@ -91,6 +95,16 @@ class Meta: blank=True, ) + @property + def latest_comment(self): + """Get the latest comment for this issue. + + Returns: + Comment | None: The most recently created comment, or None if no comments exist. + + """ + return self.comments.order_by("-nest_created_at").first() + def from_github(self, gh_issue, *, author=None, milestone=None, repository=None): """Update the instance based on GitHub issue data. diff --git a/backend/apps/mentorship/Makefile b/backend/apps/mentorship/Makefile index a43f999761..a12d49d5d7 100644 --- a/backend/apps/mentorship/Makefile +++ b/backend/apps/mentorship/Makefile @@ -3,3 +3,7 @@ mentorship-sync-module-issues: mentorship-sync-issue-levels: @CMD="python manage.py sync_issue_levels" $(MAKE) exec-backend-command + +mentorship-update-comments: + @echo "Syncing Github Comments related to issues" + @CMD="python manage.py mentorship_update_comments" $(MAKE) exec-backend-command diff --git a/backend/apps/mentorship/admin/__init__.py b/backend/apps/mentorship/admin/__init__.py index a5352c1850..9a3c365805 100644 --- a/backend/apps/mentorship/admin/__init__.py +++ b/backend/apps/mentorship/admin/__init__.py @@ -1,5 +1,6 @@ """Mentorship app admin.""" +from .issue_user_interest import IssueUserInterest from .mentee import MenteeAdmin from .mentee_program import MenteeProgramAdmin from .mentor import MentorAdmin diff --git a/backend/apps/mentorship/admin/issue_user_interest.py b/backend/apps/mentorship/admin/issue_user_interest.py new file mode 100644 index 0000000000..d26ed8d8af --- /dev/null +++ b/backend/apps/mentorship/admin/issue_user_interest.py @@ -0,0 +1,16 @@ +"""Mentorship app IssueUserInterest admin.""" + +from django.contrib import admin + +from apps.mentorship.models import IssueUserInterest + + +class IssueUserInterestAdmin(admin.ModelAdmin): + """IssueUserInterest admin.""" + + list_display = ("module", "issue") + search_fields = ("module__name", "user__login", "issue__title") + list_filter = ("module",) + + +admin.site.register(IssueUserInterest, IssueUserInterestAdmin) diff --git a/backend/apps/mentorship/management/commands/mentorship_update_comments.py b/backend/apps/mentorship/management/commands/mentorship_update_comments.py new file mode 100644 index 0000000000..229a817961 --- /dev/null +++ b/backend/apps/mentorship/management/commands/mentorship_update_comments.py @@ -0,0 +1,152 @@ +"""Sync comments for issues relevant to published mentorship modules.""" + +import logging +import re +from typing import Any + +from django.core.management.base import BaseCommand + +from apps.common.utils import truncate +from apps.github.auth import get_github_client +from apps.github.common import sync_issue_comments +from apps.github.models.issue import Issue +from apps.mentorship.models import IssueUserInterest, Module + +logger: logging.Logger = logging.getLogger(__name__) + +INTEREST_PATTERNS = [ + re.compile(r"/interested", re.IGNORECASE), +] + + +class Command(BaseCommand): + """Sync comments for issues relevant to active mentorship modules and process interests.""" + + help = "Sync comments for issues relevant to active mentorship modules and process interests" + + def handle(self, *args, **options) -> None: + """Handle the command execution.""" + self.process_mentorship_modules() + + def process_mentorship_modules(self) -> None: + """Process all active mentorship modules.""" + published_modules = Module.published_modules.all() + + if not published_modules.exists(): + self.stdout.write( + self.style.WARNING("No published mentorship modules found. Exiting.") + ) + return + + self.stdout.write(self.style.SUCCESS("Starting mentorship issue processing job...")) + + for module in published_modules: + self.stdout.write(f"\nProcessing module: {module.name}...") + self.process_module(module) + + self.stdout.write(self.style.SUCCESS("Processed successfully!")) + + def process_module(self, module: Module) -> None: + """Process a single mentorship module. + + Args: + module (Module): The module instance to process. + + """ + gh = get_github_client() + + module_repos = ( + module.project.repositories.filter(id__isnull=False) + .values_list("id", flat=True) + .distinct() + ) + + if not module_repos.exists(): + self.stdout.write( + self.style.WARNING(f"Skipping. Module '{module.name}' has no repositories.") + ) + return + + relevant_issues = Issue.objects.filter( + repository_id__in=module_repos, state=Issue.State.OPEN + ).distinct() + + self.stdout.write(f"Found {relevant_issues.count()} open issues across repositories") + + for issue in relevant_issues: + self.stdout.write( + f"Syncing comments for issue #{issue.number} '{truncate(issue.title, 20)}'" + ) + sync_issue_comments(gh, issue) + self.process_issue_interests(issue, module) + + def process_issue_interests(self, issue: Issue, module: Module) -> None: + """Process interests for a single issue. + + Args: + issue (Issue): The issue instance to process. + module (Module): The module instance. + + """ + existing_interests = IssueUserInterest.objects.filter(module=module, issue=issue) + existing_user_ids = set(existing_interests.values_list("user_id", flat=True)) + + all_comments = ( + issue.comments.select_related("author") + .filter(author__isnull=False) + .order_by("author_id", "nest_created_at") + ) + + interests_to_create = [] + interests_to_remove = [] + new_user_logins = [] + removed_user_logins = [] + + user_interest_status: dict[int, dict[str, Any]] = {} + + for comment in all_comments: + user_id = comment.author.id + entry = user_interest_status.get(user_id) + is_match = any(p.search(comment.body or "") for p in INTEREST_PATTERNS) + if entry: + entry["is_interested"] = entry["is_interested"] or is_match + else: + user_interest_status[user_id] = { + "is_interested": is_match, + "login": comment.author.login, + "author": comment.author, + } + + for user_id, status in user_interest_status.items(): + is_interested = status["is_interested"] + user_login = status["login"] + author = status["author"] + + if is_interested and user_id not in existing_user_ids: + interests_to_create.append( + IssueUserInterest(module=module, issue=issue, user=author) + ) + new_user_logins.append(user_login) + elif not is_interested and user_id in existing_user_ids: + interests_to_remove.append(user_id) + removed_user_logins.append(user_login) + + if interests_to_create: + IssueUserInterest.objects.bulk_create(interests_to_create) + self.stdout.write( + self.style.SUCCESS( + f"Registered {len(interests_to_create)} new interest(s) " + f"for issue #{issue.number}: {', '.join(new_user_logins)}" + ) + ) + + if interests_to_remove: + removed_count = IssueUserInterest.objects.filter( + module=module, issue=issue, user_id__in=interests_to_remove + ).delete()[0] + self.stdout.write( + self.style.WARNING( + f"Unregistered {removed_count} interest(s) " + f"for issue #{issue.number}: {', '.join(removed_user_logins)}" + ) + ) diff --git a/backend/apps/mentorship/migrations/0005_issueuserinterest.py b/backend/apps/mentorship/migrations/0005_issueuserinterest.py new file mode 100644 index 0000000000..d054f6c5e5 --- /dev/null +++ b/backend/apps/mentorship/migrations/0005_issueuserinterest.py @@ -0,0 +1,55 @@ +# Generated by Django 5.2.5 on 2025-09-24 13:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0036_comment"), + ("mentorship", "0004_module_key_program_key_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="IssueUserInterest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="participant_interests", + to="github.issue", + ), + ), + ( + "module", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="interests", + to="mentorship.module", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="mentorship_interests", + to="github.user", + ), + ), + ], + options={ + "verbose_name": "Issue User Interest", + "verbose_name_plural": "Issue User Interests", + "db_table": "mentorship_issue_user_interests", + "unique_together": {("module", "issue", "user")}, + }, + ), + ] diff --git a/backend/apps/mentorship/models/__init__.py b/backend/apps/mentorship/models/__init__.py index d8e5753569..f196ad9a3e 100644 --- a/backend/apps/mentorship/models/__init__.py +++ b/backend/apps/mentorship/models/__init__.py @@ -1,3 +1,4 @@ +from .issue_user_interest import IssueUserInterest from .mentee import Mentee from .mentee_module import MenteeModule from .mentee_program import MenteeProgram diff --git a/backend/apps/mentorship/models/issue_user_interest.py b/backend/apps/mentorship/models/issue_user_interest.py new file mode 100644 index 0000000000..b59128bc85 --- /dev/null +++ b/backend/apps/mentorship/models/issue_user_interest.py @@ -0,0 +1,31 @@ +"""Participant interest model.""" + +from django.db import models + + +class IssueUserInterest(models.Model): + """Represents users interested in a specific issue within a module.""" + + class Meta: + db_table = "mentorship_issue_user_interests" + verbose_name = "Issue User Interest" + verbose_name_plural = "Issue User Interests" + unique_together = ("module", "issue", "user") + + module = models.ForeignKey( + "mentorship.Module", on_delete=models.CASCADE, related_name="interests" + ) + issue = models.ForeignKey( + "github.Issue", on_delete=models.CASCADE, related_name="participant_interests" + ) + user = models.ForeignKey( + "github.User", + related_name="mentorship_interests", + on_delete=models.CASCADE, + ) + + def __str__(self): + """Return a human-readable representation of the issue user interest.""" + return ( + f"User [{self.user.login}] interested in '{self.issue.title}' for {self.module.name}" + ) diff --git a/backend/apps/mentorship/models/managers/__init__.py b/backend/apps/mentorship/models/managers/__init__.py new file mode 100644 index 0000000000..b90a81f022 --- /dev/null +++ b/backend/apps/mentorship/models/managers/__init__.py @@ -0,0 +1 @@ +from .module import PublishedModuleManager diff --git a/backend/apps/mentorship/models/managers/module.py b/backend/apps/mentorship/models/managers/module.py new file mode 100644 index 0000000000..1a203db9a1 --- /dev/null +++ b/backend/apps/mentorship/models/managers/module.py @@ -0,0 +1,13 @@ +"""Mentorship app module manager.""" + +from django.db import models + +from apps.mentorship.models.program import Program + + +class PublishedModuleManager(models.Manager): + """Published Modules.""" + + def get_queryset(self): + """Get queryset.""" + return super().get_queryset().filter(program__status=Program.ProgramStatus.PUBLISHED) diff --git a/backend/apps/mentorship/models/module.py b/backend/apps/mentorship/models/module.py index c9e13d2287..6c5263feeb 100644 --- a/backend/apps/mentorship/models/module.py +++ b/backend/apps/mentorship/models/module.py @@ -11,11 +11,15 @@ MatchingAttributes, StartEndRange, ) +from apps.mentorship.models.managers import PublishedModuleManager class Module(ExperienceLevel, MatchingAttributes, StartEndRange, TimestampedModel): """Module model representing a program unit.""" + objects = models.Manager() + published_modules = PublishedModuleManager() + class Meta: db_table = "mentorship_modules" verbose_name_plural = "Modules" From a4e9fcec122608213038c2292a35d8828646d47c Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Tue, 30 Sep 2025 15:54:36 +0530 Subject: [PATCH 03/36] fix the migration --- .../github/migrations/0037_issue_level.py | 26 --------- ...comment.py => 0037_issue_level_comment.py} | 17 +++++- .../migrations/0005_issueuserinterest.py | 55 ------------------- ...emove_task_level_module_issues_and_more.py | 46 +++++++++++++++- backend/manage.py | 5 +- 5 files changed, 63 insertions(+), 86 deletions(-) delete mode 100644 backend/apps/github/migrations/0037_issue_level.py rename backend/apps/github/migrations/{0036_comment.py => 0037_issue_level_comment.py} (77%) delete mode 100644 backend/apps/mentorship/migrations/0005_issueuserinterest.py diff --git a/backend/apps/github/migrations/0037_issue_level.py b/backend/apps/github/migrations/0037_issue_level.py deleted file mode 100644 index de309aade5..0000000000 --- a/backend/apps/github/migrations/0037_issue_level.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.2.5 on 2025-09-25 08:53 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("github", "0036_user_has_public_member_page_alter_organization_name_and_more"), - ("mentorship", "0004_module_key_program_key_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="issue", - name="level", - field=models.ForeignKey( - blank=True, - help_text="The difficulty level of this issue.", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="issues", - to="mentorship.tasklevel", - ), - ), - ] diff --git a/backend/apps/github/migrations/0036_comment.py b/backend/apps/github/migrations/0037_issue_level_comment.py similarity index 77% rename from backend/apps/github/migrations/0036_comment.py rename to backend/apps/github/migrations/0037_issue_level_comment.py index 2c67ebd898..ef9045160f 100644 --- a/backend/apps/github/migrations/0036_comment.py +++ b/backend/apps/github/migrations/0037_issue_level_comment.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.5 on 2025-09-24 13:14 +# Generated by Django 5.2.5 on 2025-09-30 10:23 import django.db.models.deletion from django.db import migrations, models @@ -7,10 +7,23 @@ class Migration(migrations.Migration): dependencies = [ ("contenttypes", "0002_remove_content_type_name"), - ("github", "0035_alter_user_bio_alter_user_is_owasp_staff"), + ("github", "0036_user_has_public_member_page_alter_organization_name_and_more"), + ("mentorship", "0004_module_key_program_key_and_more"), ] operations = [ + migrations.AddField( + model_name="issue", + name="level", + field=models.ForeignKey( + blank=True, + help_text="The difficulty level of this issue.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issues", + to="mentorship.tasklevel", + ), + ), migrations.CreateModel( name="Comment", fields=[ diff --git a/backend/apps/mentorship/migrations/0005_issueuserinterest.py b/backend/apps/mentorship/migrations/0005_issueuserinterest.py deleted file mode 100644 index d054f6c5e5..0000000000 --- a/backend/apps/mentorship/migrations/0005_issueuserinterest.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 5.2.5 on 2025-09-24 13:14 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("github", "0036_comment"), - ("mentorship", "0004_module_key_program_key_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="IssueUserInterest", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ( - "issue", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="participant_interests", - to="github.issue", - ), - ), - ( - "module", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="interests", - to="mentorship.module", - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="mentorship_interests", - to="github.user", - ), - ), - ], - options={ - "verbose_name": "Issue User Interest", - "verbose_name_plural": "Issue User Interests", - "db_table": "mentorship_issue_user_interests", - "unique_together": {("module", "issue", "user")}, - }, - ), - ] diff --git a/backend/apps/mentorship/migrations/0005_remove_task_level_module_issues_and_more.py b/backend/apps/mentorship/migrations/0005_remove_task_level_module_issues_and_more.py index 92fd8e1acf..3f3f93e1e9 100644 --- a/backend/apps/mentorship/migrations/0005_remove_task_level_module_issues_and_more.py +++ b/backend/apps/mentorship/migrations/0005_remove_task_level_module_issues_and_more.py @@ -1,11 +1,12 @@ -# Generated by Django 5.2.5 on 2025-09-25 08:53 +# Generated by Django 5.2.5 on 2025-09-30 10:23 +import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("github", "0037_issue_level"), + ("github", "0037_issue_level_comment"), ("mentorship", "0004_module_key_program_key_and_more"), ] @@ -34,4 +35,45 @@ class Migration(migrations.Migration): null=True, ), ), + migrations.CreateModel( + name="IssueUserInterest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="participant_interests", + to="github.issue", + ), + ), + ( + "module", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="interests", + to="mentorship.module", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="mentorship_interests", + to="github.user", + ), + ), + ], + options={ + "verbose_name": "Issue User Interest", + "verbose_name_plural": "Issue User Interests", + "db_table": "mentorship_issue_user_interests", + "unique_together": {("module", "issue", "user")}, + }, + ), ] diff --git a/backend/manage.py b/backend/manage.py index 83d4673775..b093d1c8f4 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -5,10 +5,13 @@ import os import sys +from dotenv import load_dotenv + +load_dotenv() if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.local") os.environ.setdefault("DJANGO_CONFIGURATION", "Local") - + load_dotenv() from configurations.management import execute_from_command_line execute_from_command_line(sys.argv) From e69ba465f8971e5a4ab17b74b897b64b0bdf74bf Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Tue, 30 Sep 2025 18:21:45 +0530 Subject: [PATCH 04/36] added view all issues page --- .../apps/github/api/internal/nodes/issue.py | 10 + .../mentorship/api/internal/nodes/module.py | 6 + .../modules/[moduleKey]/issues/page.tsx | 214 ++++++++++++++++++ frontend/src/components/CardDetailsPage.tsx | 29 ++- frontend/src/server/queries/moduleQueries.ts | 30 +++ 5 files changed, 280 insertions(+), 9 deletions(-) create mode 100644 frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 4ac7eb7672..ac34b373a4 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -37,3 +37,13 @@ def organization_name(self) -> str | None: def repository_name(self) -> str | None: """Resolve the repository name.""" return self.repository.name if self.repository else None + + @strawberry.field + def assignees(self) -> list[UserNode]: + """Resolve assignees list.""" + return list(self.assignees.all()) + + @strawberry.field + def labels(self) -> list[str]: + """Resolve label names for the issue.""" + return list(self.labels.values_list("name", flat=True)) diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index fa94ad4728..d635d30cfb 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -4,6 +4,7 @@ import strawberry +from apps.github.api.internal.nodes.issue import IssueNode from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum from apps.mentorship.api.internal.nodes.mentor import MentorNode from apps.mentorship.api.internal.nodes.program import ProgramNode @@ -35,6 +36,11 @@ def project_name(self) -> str | None: """Get the project name for this module.""" return self.project.name if self.project else None + @strawberry.field + def issues(self) -> list[IssueNode]: + """Return issues linked to this module.""" + return list(self.issues.select_related("repository", "author").order_by("-created_at")) + @strawberry.input class CreateModuleInput: diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx new file mode 100644 index 0000000000..592b17f649 --- /dev/null +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -0,0 +1,214 @@ +'use client' + +import { useQuery } from '@apollo/client' +import { Select, SelectItem } from '@heroui/select' +import Image from 'next/image' +import { useParams, useRouter, useSearchParams } from 'next/navigation' +import { useEffect, useMemo, useState } from 'react' +import { ErrorDisplay, handleAppError } from 'app/global-error' +import { GET_MODULE_ISSUES } from 'server/queries/moduleQueries' +import type { Issue } from 'types/issue' +import LoadingSpinner from 'components/LoadingSpinner' +import { TruncatedText } from 'components/TruncatedText' + +const LABEL_ALL = 'all' + +const IssuesPage = () => { + const { programKey, moduleKey } = useParams() as { programKey: string; moduleKey: string } + const router = useRouter() + const searchParams = useSearchParams() + const [selectedLabel, setSelectedLabel] = useState(searchParams.get('label') || LABEL_ALL) + + const { data, loading, error } = useQuery(GET_MODULE_ISSUES, { + variables: { programKey, moduleKey }, + skip: !programKey || !moduleKey, + fetchPolicy: 'cache-and-network', + }) + + useEffect(() => { + if (error) handleAppError(error) + }, [error]) + + const moduleData = data?.getModule + const moduleIssues: Issue[] = useMemo(() => { + const issues = (moduleData?.issues || []).map((i) => ({ + author: i.author, + createdAt: i.createdAt, + hint: '', + labels: i.labels || [], + number: undefined, + organizationName: i.organizationName, + projectName: moduleData?.projectName || '', + projectUrl: '', + repository: undefined, + repositoryLanguages: [], + summary: '', + title: i.title, + updatedAt: i.createdAt, + url: i.url, + objectID: i.id, + })) + if (selectedLabel === LABEL_ALL) return issues + return issues.filter((iss) => (iss.labels || []).includes(selectedLabel)) + }, [moduleData, selectedLabel]) + + const allLabels: string[] = useMemo(() => { + const labels = new Set() + ;(moduleData?.issues || []).forEach((i) => + (i.labels || []).forEach((l: string) => labels.add(l)) + ) + return Array.from(labels).sort() + }, [moduleData]) + + const handleLabelChange = (label: string) => { + setSelectedLabel(label) + const params = new URLSearchParams(searchParams.toString()) + if (label === LABEL_ALL) { + params.delete('label') + } else { + params.set('label', label) + } + router.replace(`?${params.toString()}`) + } + + if (loading) return + if (!moduleData) + return + + return ( +
+
+
+

{moduleData.name} Issues

+
+ +
+
+ +
+ + + + + + + + + + {moduleIssues.map((issue) => ( + + + + + + ))} + {moduleIssues.length === 0 && ( + + + + )} + +
+ Title + + Labels + + Assignee +
+ + 50 ? '…' : ''}`} + /> + + +
+ {(() => { + const labels = issue.labels || [] + const visible = labels.slice(0, 5) + const remaining = labels.length - visible.length + return ( + <> + {visible.map((label) => ( + + {label} + + ))} + {remaining > 0 && ( + + +{remaining} more + + )} + + ) + })()} +
+
+ {(data?.getModule?.issues || []) + .find((i) => i.id === issue.objectID) + ?.assignees?.slice(0, 1) + .map((a) => ( +
+ {a.login} + {a.login || a.name} +
+ )) || Unassigned} +
+ No issues found for the selected filter. +
+
+
+
+ ) +} + +export default IssuesPage diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 2ba935db21..7a9ec01234 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -84,15 +84,26 @@ const DetailsCard = ({ admins?.some( (admin) => admin.login === ((data as ExtendedSession)?.user?.login as string) ) && ( - +
+ + +
)} {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( Date: Thu, 2 Oct 2025 15:22:18 +0530 Subject: [PATCH 05/36] added queries and pages --- .../apps/github/api/internal/nodes/issue.py | 8 + .../api/internal/mutations/module.py | 91 +++++++ .../mentorship/api/internal/nodes/module.py | 31 ++- .../[moduleKey]/issues/[issueId]/page.tsx | 230 ++++++++++++++++++ .../modules/[moduleKey]/issues/page.tsx | 59 +++-- frontend/src/server/queries/issueQueries.ts | 75 ++++++ frontend/src/server/queries/moduleQueries.ts | 3 +- 7 files changed, 472 insertions(+), 25 deletions(-) create mode 100644 frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx create mode 100644 frontend/src/server/queries/issueQueries.ts diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index ac34b373a4..52c816dc32 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -5,13 +5,16 @@ from apps.github.api.internal.nodes.user import UserNode from apps.github.models.issue import Issue +from apps.mentorship.models import IssueUserInterest # add import @strawberry_django.type( Issue, fields=[ "created_at", + "number", "state", + "summary", "title", "url", ], @@ -47,3 +50,8 @@ def assignees(self) -> list[UserNode]: def labels(self) -> list[str]: """Resolve label names for the issue.""" return list(self.labels.values_list("name", flat=True)) + + @strawberry.field + def interested_users(self) -> list[UserNode]: + """Return all users who have expressed interest in this issue.""" + return [interest.user for interest in IssueUserInterest.objects.filter(issue=self)] diff --git a/backend/apps/mentorship/api/internal/mutations/module.py b/backend/apps/mentorship/api/internal/mutations/module.py index b185f10148..7edeabad55 100644 --- a/backend/apps/mentorship/api/internal/mutations/module.py +++ b/backend/apps/mentorship/api/internal/mutations/module.py @@ -14,6 +14,7 @@ UpdateModuleInput, ) from apps.mentorship.models import Mentor, Module, Program +from apps.mentorship.models.issue_user_interest import IssueUserInterest from apps.nest.api.internal.permissions import IsAuthenticated from apps.owasp.models import Project @@ -114,6 +115,96 @@ def create_module(self, info: strawberry.Info, input_data: CreateModuleInput) -> return module + @strawberry.mutation(permission_classes=[IsAuthenticated]) + @transaction.atomic + def assign_issue_to_user( + self, + info: strawberry.Info, + *, + module_key: str, + program_key: str, + issue_id: int, + user_login: str, + ) -> ModuleNode: + """Assign an issue to a user by updating Issue.assignees within the module scope.""" + user = info.context.request.user + + module = ( + Module.objects.select_related("program") + .filter(key=module_key, program__key=program_key) + .first() + ) + if module is None: + msg = "Module not found." + raise ObjectDoesNotExist(msg) + + mentor = Mentor.objects.filter(nest_user=user).first() + if mentor is None: + msg = "Only mentors can assign issues." + raise PermissionDenied(msg) + if not module.program.admins.filter(id=mentor.id).exists(): + raise PermissionDenied + + gh_user = GithubUser.objects.filter(login=user_login).first() + if gh_user is None: + msg = "Assignee not found." + raise ObjectDoesNotExist(msg) + + issue = module.issues.filter(id=issue_id).first() + if issue is None: + msg = "Issue not found in this module." + raise ObjectDoesNotExist(msg) + + issue.assignees.add(gh_user) + + IssueUserInterest.objects.filter(module=module, issue_id=issue.id, user=gh_user).delete() + + return module + + @strawberry.mutation(permission_classes=[IsAuthenticated]) + @transaction.atomic + def unassign_issue_from_user( + self, + info: strawberry.Info, + *, + module_key: str, + program_key: str, + issue_id: int, + user_login: str, + ) -> ModuleNode: + """Unassign an issue from a user by updating Issue.assignees within the module scope.""" + user = info.context.request.user + + module = ( + Module.objects.select_related("program") + .filter(key=module_key, program__key=program_key) + .first() + ) + if module is None: + msg = "Module not found." + raise ObjectDoesNotExist(msg) + + mentor = Mentor.objects.filter(nest_user=user).first() + if mentor is None: + msg = "Only mentors can unassign issues." + raise PermissionDenied(msg) + if not module.program.admins.filter(id=mentor.id).exists(): + raise PermissionDenied + + gh_user = GithubUser.objects.filter(login=user_login).first() + if gh_user is None: + msg = "Assignee not found." + raise ObjectDoesNotExist(msg) + + issue = module.issues.filter(id=issue_id).first() + if issue is None: + msg = f"Issue {issue_id} not found in this module." + raise ObjectDoesNotExist(msg) + + issue.assignees.remove(gh_user) + + return module + @strawberry.mutation(permission_classes=[IsAuthenticated]) @transaction.atomic def update_module(self, info: strawberry.Info, input_data: UpdateModuleInput) -> ModuleNode: diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index d635d30cfb..b1601a3cf6 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -5,9 +5,11 @@ import strawberry from apps.github.api.internal.nodes.issue import IssueNode +from apps.github.api.internal.nodes.user import UserNode from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum from apps.mentorship.api.internal.nodes.mentor import MentorNode from apps.mentorship.api.internal.nodes.program import ProgramNode +from apps.mentorship.models.issue_user_interest import IssueUserInterest @strawberry.type @@ -39,7 +41,34 @@ def project_name(self) -> str | None: @strawberry.field def issues(self) -> list[IssueNode]: """Return issues linked to this module.""" - return list(self.issues.select_related("repository", "author").order_by("-created_at")) + return list( + self.issues.select_related("repository", "author") + .prefetch_related("assignees", "labels") + .order_by("-created_at") + ) + + @strawberry.field + def issue_by_number(self, number: int) -> IssueNode | None: + """Return a single issue by its GitHub number within this module's linked issues.""" + return ( + self.issues.select_related("repository", "author") + .prefetch_related("assignees", "labels") + .filter(number=number) + .first() + ) + + @strawberry.field + def interested_users(self, issue_number: int) -> list[UserNode]: + """Return users interested in this module's issue identified by its number.""" + issue_ids = list(self.issues.filter(number=issue_number).values_list("id", flat=True)) + if not issue_ids: + return [] + interests = ( + IssueUserInterest.objects.select_related("user") + .filter(module=self, issue_id__in=issue_ids) + .order_by("user__login") + ) + return [i.user for i in interests] @strawberry.input diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx new file mode 100644 index 0000000000..3cbc12448d --- /dev/null +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -0,0 +1,230 @@ +'use client' + +import { useMutation, useQuery } from '@apollo/client' +import { faLink, faPlus, faTags, faUsers, faXmark } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import Image from 'next/image' +import Link from 'next/link' +import { useParams } from 'next/navigation' +import { useMemo } from 'react' +import { ErrorDisplay } from 'app/global-error' +import { + ASSIGN_ISSUE_TO_USER, + GET_MODULE_ISSUE_VIEW, + UNASSIGN_ISSUE_FROM_USER, +} from 'server/queries/issueQueries' +import ActionButton from 'components/ActionButton' +import AnchorTitle from 'components/AnchorTitle' +import LoadingSpinner from 'components/LoadingSpinner' +import SecondaryCard from 'components/SecondaryCard' +import { TruncatedText } from 'components/TruncatedText' + +const ModuleIssueDetailsPage = () => { + const { issueId } = useParams() as { issueId: string } + + const { programKey, moduleKey } = useParams() as { + programKey: string + moduleKey: string + issueId: string + } + const { data, loading } = useQuery(GET_MODULE_ISSUE_VIEW, { + variables: { programKey, moduleKey, number: Number(issueId) }, + skip: !issueId, + fetchPolicy: 'cache-first', + nextFetchPolicy: 'cache-and-network', + }) + + const [assignIssue, { loading: assigning }] = useMutation(ASSIGN_ISSUE_TO_USER, { + refetchQueries: [ + { + query: GET_MODULE_ISSUE_VIEW, + variables: { programKey, moduleKey, number: Number(issueId) }, + }, + ], + awaitRefetchQueries: true, + }) + const [unassignIssue, { loading: unassigning }] = useMutation(UNASSIGN_ISSUE_FROM_USER, { + refetchQueries: [ + { + query: GET_MODULE_ISSUE_VIEW, + variables: { programKey, moduleKey, number: Number(issueId) }, + }, + ], + awaitRefetchQueries: true, + }) + + const issue = useMemo(() => data?.getModule?.issueByNumber || null, [data]) + const issueGlobalId = issue?.id as string | undefined + const issuePk = useMemo(() => { + if (!issueGlobalId) return null + try { + const decoded = atob(issueGlobalId) + const pk = parseInt(decoded.split(':').pop() || '', 10) + return Number.isNaN(pk) ? null : pk + } catch { + return null + } + }, [issueGlobalId]) + + if (loading) return + if (!issue) + return + + const assignees = issue.assignees || [] + const labels: string[] = issue.labels || [] + const visible = labels.slice(0, 5) + const remaining = labels.length - visible.length + + return ( +
+
+
+
+

+ +

+
+ {issue.organizationName}/{issue.repositoryName} • #{issue.number} +
+
+ + View on GitHub + +
+ + }> +
+ {issue.summary || 'No description.'} +
+
+ +
+

+
+
+ +
+ Labels +
+

+
+ {visible.map((l: string, index) => ( + + ))} + {remaining > 0 && ( + + )} +
+
+ +
+

+
+
+ +
+ Assignees +
+

+
+ {assignees.map((a) => ( +
+ + {a.login} + {a.login || a.name} + + +
+ ))} + {assignees.length === 0 && Unassigned} +
+
+ +
+

+
+
+ +
+ Interested Users +
+

+
+ {(data?.getModule?.interestedUsers || []).map((u) => ( +
+
+ {u.login} + @{u.login} +
+ +
+ ))} + {(data?.getModule?.interestedUsers || []).length === 0 && ( + No interested users yet. + )} +
+
+
+
+ ) +} + +export default ModuleIssueDetailsPage diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 592b17f649..9a2cf8aaf5 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -36,7 +36,7 @@ const IssuesPage = () => { createdAt: i.createdAt, hint: '', labels: i.labels || [], - number: undefined, + number: i.number, organizationName: i.organizationName, projectName: moduleData?.projectName || '', projectUrl: '', @@ -137,16 +137,19 @@ const IssuesPage = () => { {moduleIssues.map((issue) => ( - + router.push( + `/my/mentorship/programs/${programKey}/modules/${moduleKey}/issues/${issue.number}` + ) + } + className="block max-w-xl text-left hover:underline" > 50 ? '…' : ''}`} /> - +
@@ -159,13 +162,13 @@ const IssuesPage = () => { {visible.map((label) => ( {label} ))} {remaining > 0 && ( - + +{remaining} more )} @@ -175,21 +178,31 @@ const IssuesPage = () => {
- {(data?.getModule?.issues || []) - .find((i) => i.id === issue.objectID) - ?.assignees?.slice(0, 1) - .map((a) => ( -
- {a.login} - {a.login || a.name} + {((assignees) => + assignees?.length ? ( +
+
+ {assignees[0].login} + {assignees[0].login || assignees[0].name} +
+ {assignees.length > 1 && ( +
+ +{assignees.length - 1} +
+ )}
- )) || Unassigned} + ) : ( + Unassigned + ))( + (data?.getModule?.issues || []).find((i) => i.id === issue.objectID) + ?.assignees + )} ))} diff --git a/frontend/src/server/queries/issueQueries.ts b/frontend/src/server/queries/issueQueries.ts new file mode 100644 index 0000000000..457cbb2c91 --- /dev/null +++ b/frontend/src/server/queries/issueQueries.ts @@ -0,0 +1,75 @@ +import { gql } from '@apollo/client' + +export const GET_MODULE_ISSUE_VIEW = gql` + query GetModuleIssueView($programKey: String!, $moduleKey: String!, $number: Int!) { + getModule(programKey: $programKey, moduleKey: $moduleKey) { + id + issueByNumber(number: $number) { + id + number + title + summary + url + state + createdAt + organizationName + repositoryName + author { + id + login + name + avatarUrl + } + assignees { + id + login + name + avatarUrl + } + labels + } + interestedUsers(issueNumber: $number) { + id + login + name + avatarUrl + } + } + } +` + +export const ASSIGN_ISSUE_TO_USER = gql` + mutation AssignIssueToUser( + $programKey: String! + $moduleKey: String! + $issueId: Int! + $userLogin: String! + ) { + assignIssueToUser( + programKey: $programKey + moduleKey: $moduleKey + issueId: $issueId + userLogin: $userLogin + ) { + id + } + } +` + +export const UNASSIGN_ISSUE_FROM_USER = gql` + mutation UnassignIssueFromUser( + $programKey: String! + $moduleKey: String! + $issueId: Int! + $userLogin: String! + ) { + unassignIssueFromUser( + programKey: $programKey + moduleKey: $moduleKey + issueId: $issueId + userLogin: $userLogin + ) { + id + } + } +` diff --git a/frontend/src/server/queries/moduleQueries.ts b/frontend/src/server/queries/moduleQueries.ts index 0171c215bf..f1c4d88bc6 100644 --- a/frontend/src/server/queries/moduleQueries.ts +++ b/frontend/src/server/queries/moduleQueries.ts @@ -82,11 +82,12 @@ export const GET_MODULE_ISSUES = gql` id name key - tags issues { id + number createdAt title + summary url author { id From 3c6a06b86c3c08ddd3dc08c5a48c2ec273279fc5 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Thu, 2 Oct 2025 15:30:23 +0530 Subject: [PATCH 06/36] fix error handling --- .../api/internal/mutations/module.py | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/backend/apps/mentorship/api/internal/mutations/module.py b/backend/apps/mentorship/api/internal/mutations/module.py index 7edeabad55..05eed87e2f 100644 --- a/backend/apps/mentorship/api/internal/mutations/module.py +++ b/backend/apps/mentorship/api/internal/mutations/module.py @@ -135,25 +135,21 @@ def assign_issue_to_user( .first() ) if module is None: - msg = "Module not found." - raise ObjectDoesNotExist(msg) + raise ObjectDoesNotExist(msg="Module not found.") mentor = Mentor.objects.filter(nest_user=user).first() if mentor is None: - msg = "Only mentors can assign issues." - raise PermissionDenied(msg) + raise PermissionDenied(msg="Only mentors can assign issues.") if not module.program.admins.filter(id=mentor.id).exists(): raise PermissionDenied gh_user = GithubUser.objects.filter(login=user_login).first() if gh_user is None: - msg = "Assignee not found." - raise ObjectDoesNotExist(msg) + raise ObjectDoesNotExist(msg="Assignee not found.") issue = module.issues.filter(id=issue_id).first() if issue is None: - msg = "Issue not found in this module." - raise ObjectDoesNotExist(msg) + raise ObjectDoesNotExist(msg="Issue not found in this module.") issue.assignees.add(gh_user) @@ -181,25 +177,21 @@ def unassign_issue_from_user( .first() ) if module is None: - msg = "Module not found." - raise ObjectDoesNotExist(msg) + raise ObjectDoesNotExist mentor = Mentor.objects.filter(nest_user=user).first() if mentor is None: - msg = "Only mentors can unassign issues." - raise PermissionDenied(msg) + raise PermissionDenied if not module.program.admins.filter(id=mentor.id).exists(): raise PermissionDenied gh_user = GithubUser.objects.filter(login=user_login).first() if gh_user is None: - msg = "Assignee not found." - raise ObjectDoesNotExist(msg) + raise ObjectDoesNotExist(msg="Assignee not found.") issue = module.issues.filter(id=issue_id).first() if issue is None: - msg = f"Issue {issue_id} not found in this module." - raise ObjectDoesNotExist(msg) + raise ObjectDoesNotExist(msg=f"Issue {issue_id} not found in this module.") issue.assignees.remove(gh_user) @@ -216,19 +208,17 @@ def update_module(self, info: strawberry.Info, input_data: UpdateModuleInput) -> key=input_data.key, program__key=input_data.program_key ) except Module.DoesNotExist as e: - msg = "Module not found." - raise ObjectDoesNotExist(msg) from e + raise ObjectDoesNotExist(msg="Module not found.") from e try: creator_as_mentor = Mentor.objects.get(nest_user=user) except Mentor.DoesNotExist as err: - msg = "Only mentors can edit modules." logger.warning( "User '%s' is not a mentor and cannot edit modules.", user.username, exc_info=True, ) - raise PermissionDenied(msg) from err + raise PermissionDenied(msg="Only mentors can edit modules.") from err if not module.program.admins.filter(id=creator_as_mentor.id).exists(): raise PermissionDenied From 553fecb8168f3651d95498298ce3ae13745dd401 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Mon, 6 Oct 2025 17:31:36 +0530 Subject: [PATCH 07/36] added pr and issue linking command --- backend/apps/github/Makefile | 4 ++ backend/apps/github/admin/pull_request.py | 1 + .../commands/github_update_pull_requests.py | 60 +++++++++++++++++++ .../0038_pullrequest_related_issues.py | 19 ++++++ backend/apps/github/models/pull_request.py | 6 ++ 5 files changed, 90 insertions(+) create mode 100644 backend/apps/github/management/commands/github_update_pull_requests.py create mode 100644 backend/apps/github/migrations/0038_pullrequest_related_issues.py diff --git a/backend/apps/github/Makefile b/backend/apps/github/Makefile index 018876262e..e0ea5e7def 100644 --- a/backend/apps/github/Makefile +++ b/backend/apps/github/Makefile @@ -17,3 +17,7 @@ github-update-related-organizations: github-update-users: @echo "Updating GitHub users" @CMD="python manage.py github_update_users" $(MAKE) exec-backend-command + +github-update-pull-requests: + @echo "Linking pull requests to issues using closing keywords" + @CMD="python manage.py github_update_pull_requests" $(MAKE) exec-backend-command diff --git a/backend/apps/github/admin/pull_request.py b/backend/apps/github/admin/pull_request.py index 99a8194951..c177b4c987 100644 --- a/backend/apps/github/admin/pull_request.py +++ b/backend/apps/github/admin/pull_request.py @@ -14,6 +14,7 @@ class PullRequestAdmin(admin.ModelAdmin): "author", "labels", "repository", + "related_issues", ) list_display = ( "repository", diff --git a/backend/apps/github/management/commands/github_update_pull_requests.py b/backend/apps/github/management/commands/github_update_pull_requests.py new file mode 100644 index 0000000000..d41593b288 --- /dev/null +++ b/backend/apps/github/management/commands/github_update_pull_requests.py @@ -0,0 +1,60 @@ +"""Link pull requests to issues via closing keywords in PR body (e.g., 'closes #123').""" + +import logging +import re + +from django.core.management.base import BaseCommand + +from apps.github.models.issue import Issue +from apps.github.models.pull_request import PullRequest + +logger: logging.Logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Link pull requests to issues via closing keywords in PR body (e.g., 'closes #123')." + + # regex pattern to find the linked issue + pattern = re.compile( + r"\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\b\s+" + r"#(\d+)", + re.IGNORECASE, + ) + + def handle(self, *args, **options): + linked = 0 + updated_prs = [] + + logger.info("Linking PRs to issues using closing keywords") + + queryset = PullRequest.objects.select_related("repository").all() + + for pr in queryset: + if not pr.repository: + logger.info("Skipping PR #%s: no repository", pr.number) + continue + + body = pr.body or "" + matches = self.pattern.findall(body) + if not matches: + logger.info("No closing keyword pattern found for PR #%s", pr.number) + continue + issue_numbers = {int(n) for n in matches} + + issues = list(Issue.objects.filter(repository=pr.repository, number__in=issue_numbers)) + + existing_ids = set(pr.related_issues.values_list("id", flat=True)) + new_ids = {i.id for i in issues} - existing_ids + if new_ids: + pr.related_issues.add(*new_ids) + linked += len(new_ids) + updated_prs.append(pr) + self.stdout.write( + f"Linked PR #{pr.number} ({pr.repository.name}) -> Issues " + + ", ".join(f"#{i.number}" for i in issues if i.id in new_ids) + ) + + if updated_prs: + PullRequest.bulk_save(updated_prs) + + self.stdout.write(f"Linked: {linked}") diff --git a/backend/apps/github/migrations/0038_pullrequest_related_issues.py b/backend/apps/github/migrations/0038_pullrequest_related_issues.py new file mode 100644 index 0000000000..af5a5bd7f4 --- /dev/null +++ b/backend/apps/github/migrations/0038_pullrequest_related_issues.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.5 on 2025-10-06 12:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0037_issue_level_comment"), + ] + + operations = [ + migrations.AddField( + model_name="pullrequest", + name="related_issues", + field=models.ManyToManyField( + blank=True, related_name="pull_requests", to="github.issue", verbose_name="Issues" + ), + ), + ] diff --git a/backend/apps/github/models/pull_request.py b/backend/apps/github/models/pull_request.py index 84e1deac9e..e3677880d9 100644 --- a/backend/apps/github/models/pull_request.py +++ b/backend/apps/github/models/pull_request.py @@ -61,6 +61,12 @@ class Meta: related_name="pull_request_labels", blank=True, ) + related_issues = models.ManyToManyField( + "github.Issue", + verbose_name="Issues", + related_name="pull_requests", + blank=True, + ) def from_github(self, gh_pull_request, *, author=None, milestone=None, repository=None): """Update the instance based on GitHub pull request data. From 2e8aaedcccf6b50137dfe9de4b0f6aa7fadd5f27 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Tue, 7 Oct 2025 22:50:12 +0530 Subject: [PATCH 08/36] update ui added pull requests card --- .../apps/github/api/internal/nodes/issue.py | 6 ++ .../[moduleKey]/issues/[issueId]/page.tsx | 100 +++++++++++++----- frontend/src/server/queries/issueQueries.ts | 12 +++ frontend/src/server/queries/moduleQueries.ts | 12 +++ frontend/src/types/issue.ts | 2 + frontend/src/types/pullRequest.ts | 1 + 6 files changed, 104 insertions(+), 29 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 52c816dc32..1b4d89d232 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.github.api.internal.nodes.pull_request import PullRequestNode from apps.github.api.internal.nodes.user import UserNode from apps.github.models.issue import Issue from apps.mentorship.models import IssueUserInterest # add import @@ -55,3 +56,8 @@ def labels(self) -> list[str]: def interested_users(self) -> list[UserNode]: """Return all users who have expressed interest in this issue.""" return [interest.user for interest in IssueUserInterest.objects.filter(issue=self)] + + @strawberry.field + def pull_requests(self) -> list[PullRequestNode]: + """Return all pull requests linked to this issue.""" + return list(self.pull_requests.select_related("author", "repository").all()) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 3cbc12448d..281391a13d 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -1,12 +1,18 @@ 'use client' import { useMutation, useQuery } from '@apollo/client' -import { faLink, faPlus, faTags, faUsers, faXmark } from '@fortawesome/free-solid-svg-icons' +import { + faCodeBranch, + faLink, + faPlus, + faTags, + faUsers, + faXmark, +} from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Image from 'next/image' import Link from 'next/link' import { useParams } from 'next/navigation' -import { useMemo } from 'react' import { ErrorDisplay } from 'app/global-error' import { ASSIGN_ISSUE_TO_USER, @@ -53,27 +59,28 @@ const ModuleIssueDetailsPage = () => { awaitRefetchQueries: true, }) - const issue = useMemo(() => data?.getModule?.issueByNumber || null, [data]) - const issueGlobalId = issue?.id as string | undefined - const issuePk = useMemo(() => { - if (!issueGlobalId) return null - try { - const decoded = atob(issueGlobalId) - const pk = parseInt(decoded.split(':').pop() || '', 10) - return Number.isNaN(pk) ? null : pk - } catch { - return null - } - }, [issueGlobalId]) + const issue = data?.getModule?.issueByNumber + + const issuePk = issue?.getModule?.id if (loading) return if (!issue) return const assignees = issue.assignees || [] - const labels: string[] = issue.labels || [] - const visible = labels.slice(0, 5) - const remaining = labels.length - visible.length + const labels = issue.labels || [] + const visibleLabels = labels.slice(0, 5) + const remainingLabels = labels.length - visibleLabels.length + + const getButtonClassName = (disabled: boolean) => + `inline-flex items-center justify-center rounded-md border p-1.5 text-sm ${ + disabled + ? 'cursor-not-allowed border-gray-300 text-gray-400 dark:border-gray-600' + : 'border-gray-300 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800' + }` + + const labelButtonClassName = + 'rounded-lg border border-gray-400 px-3 py-1 text-sm hover:bg-gray-200 dark:border-gray-300 dark:hover:bg-gray-700' return (
@@ -108,18 +115,13 @@ const ModuleIssueDetailsPage = () => {
- {visible.map((l: string, index) => ( - ))} - {remaining > 0 && ( - + {remainingLabels > 0 && ( + )}
@@ -162,7 +164,7 @@ const ModuleIssueDetailsPage = () => { variables: { programKey, moduleKey, issueId: issuePk, userLogin: a.login }, }) }} - className={`inline-flex items-center justify-center rounded-md border p-1.5 text-sm ${!issuePk ? 'cursor-not-allowed border-gray-300 text-gray-400 dark:border-gray-600' : 'border-gray-300 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800'}`} + className={getButtonClassName(!issuePk || unassigning)} title={unassigning ? 'Unassigning…' : `Unassign @${a.login}`} > @@ -173,6 +175,46 @@ const ModuleIssueDetailsPage = () => { + +
+ {issue.pullRequests?.map((pr) => ( +
+
+ {pr.author?.login +
+ + {pr.title} + +
+ by {pr.author?.login || 'Unknown'} •{' '} + {new Date(pr.createdAt).toLocaleDateString()} +
+
+
+ + + View PR + +
+ )) || No linked pull requests.} +
+
+

@@ -207,7 +249,7 @@ const ModuleIssueDetailsPage = () => { variables: { programKey, moduleKey, issueId: issuePk, userLogin: u.login }, }) }} - className={`inline-flex items-center gap-1.5 rounded-md border px-3 py-1 text-sm ${!issuePk ? 'cursor-not-allowed border-gray-300 text-gray-400 dark:border-gray-600' : 'border-gray-300 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800'}`} + className={`${getButtonClassName(!issuePk || assigning)} px-3 py-1`} title={ !issuePk ? 'Loading issue…' : assigning ? 'Assigning…' : 'Assign to this user' } diff --git a/frontend/src/server/queries/issueQueries.ts b/frontend/src/server/queries/issueQueries.ts index 457cbb2c91..473a4c133f 100644 --- a/frontend/src/server/queries/issueQueries.ts +++ b/frontend/src/server/queries/issueQueries.ts @@ -27,6 +27,18 @@ export const GET_MODULE_ISSUE_VIEW = gql` avatarUrl } labels + pullRequests { + id + title + url + createdAt + author { + id + login + name + avatarUrl + } + } } interestedUsers(issueNumber: $number) { id diff --git a/frontend/src/server/queries/moduleQueries.ts b/frontend/src/server/queries/moduleQueries.ts index f1c4d88bc6..70839f3d70 100644 --- a/frontend/src/server/queries/moduleQueries.ts +++ b/frontend/src/server/queries/moduleQueries.ts @@ -102,6 +102,18 @@ export const GET_MODULE_ISSUES = gql` name } labels + pullRequests { + id + title + url + createdAt + author { + id + login + name + avatarUrl + } + } } } } diff --git a/frontend/src/types/issue.ts b/frontend/src/types/issue.ts index bd8f746f0c..5be98aaf30 100644 --- a/frontend/src/types/issue.ts +++ b/frontend/src/types/issue.ts @@ -1,3 +1,4 @@ +import type { PullRequest } from 'types/pullRequest' import type { RepositoryDetails, User } from 'types/user' export type Issue = { @@ -9,6 +10,7 @@ export type Issue = { organizationName?: string projectName: string projectUrl: string + pullRequests?: PullRequest[] repository?: RepositoryDetails repositoryLanguages?: string[] summary: string diff --git a/frontend/src/types/pullRequest.ts b/frontend/src/types/pullRequest.ts index 47c15ad798..ccd796587c 100644 --- a/frontend/src/types/pullRequest.ts +++ b/frontend/src/types/pullRequest.ts @@ -1,6 +1,7 @@ import type { User } from 'types/user' export type PullRequest = { + id: string author: User createdAt: string organizationName: string From 5cd30e27597d06e0c5f5a02006ecb5b2b1902285 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 8 Oct 2025 04:47:37 +0530 Subject: [PATCH 09/36] update code --- .../api/internal/mutations/module.py | 10 ++-- .../mentorship/api/internal/nodes/module.py | 23 +++++++++ .../[moduleKey]/issues/[issueId]/page.tsx | 47 ++++++++++++++----- frontend/src/server/queries/issueQueries.ts | 10 ++-- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/backend/apps/mentorship/api/internal/mutations/module.py b/backend/apps/mentorship/api/internal/mutations/module.py index 05eed87e2f..ed81af8d02 100644 --- a/backend/apps/mentorship/api/internal/mutations/module.py +++ b/backend/apps/mentorship/api/internal/mutations/module.py @@ -123,7 +123,7 @@ def assign_issue_to_user( *, module_key: str, program_key: str, - issue_id: int, + issue_number: int, user_login: str, ) -> ModuleNode: """Assign an issue to a user by updating Issue.assignees within the module scope.""" @@ -147,7 +147,7 @@ def assign_issue_to_user( if gh_user is None: raise ObjectDoesNotExist(msg="Assignee not found.") - issue = module.issues.filter(id=issue_id).first() + issue = module.issues.filter(number=issue_number).first() if issue is None: raise ObjectDoesNotExist(msg="Issue not found in this module.") @@ -165,7 +165,7 @@ def unassign_issue_from_user( *, module_key: str, program_key: str, - issue_id: int, + issue_number: int, user_login: str, ) -> ModuleNode: """Unassign an issue from a user by updating Issue.assignees within the module scope.""" @@ -189,9 +189,9 @@ def unassign_issue_from_user( if gh_user is None: raise ObjectDoesNotExist(msg="Assignee not found.") - issue = module.issues.filter(id=issue_id).first() + issue = module.issues.filter(number=issue_number).first() if issue is None: - raise ObjectDoesNotExist(msg=f"Issue {issue_id} not found in this module.") + raise ObjectDoesNotExist(msg=f"Issue {issue_number} not found in this module.") issue.assignees.remove(gh_user) diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index b1601a3cf6..17ad8f82ba 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -10,6 +10,7 @@ from apps.mentorship.api.internal.nodes.mentor import MentorNode from apps.mentorship.api.internal.nodes.program import ProgramNode from apps.mentorship.models.issue_user_interest import IssueUserInterest +from apps.mentorship.models.task import Task @strawberry.type @@ -70,6 +71,28 @@ def interested_users(self, issue_number: int) -> list[UserNode]: ) return [i.user for i in interests] + @strawberry.field + def task_deadline(self, issue_number: int) -> datetime | None: + """Return the earliest deadline for tasks linked to this module and issue number.""" + return ( + Task.objects.filter(issue__number=issue_number) + .order_by("deadline_at") + .values_list("deadline_at", flat=True) + .first() + ) + + @strawberry.field + def task_assigned_at(self, issue_number: int) -> datetime | None: + """Return the earliest assignment time for tasks linked to this module and issue number.""" + return ( + Task.objects.filter( + issue__number=issue_number, + ) + .order_by("assigned_at") + .values_list("assigned_at", flat=True) + .first() + ) + @strawberry.input class CreateModuleInput: diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 281391a13d..cc668e6c68 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -61,8 +61,6 @@ const ModuleIssueDetailsPage = () => { const issue = data?.getModule?.issueByNumber - const issuePk = issue?.getModule?.id - if (loading) return if (!issue) return @@ -105,6 +103,23 @@ const ModuleIssueDetailsPage = () => {
+ }> +
+
+ Assigned:{' '} + {data?.getModule?.taskAssignedAt + ? new Date(data.getModule.taskAssignedAt).toLocaleDateString() + : 'Not assigned'} +
+
+ Deadline:{' '} + {data?.getModule?.taskDeadline + ? new Date(data.getModule.taskDeadline).toLocaleDateString() + : 'No deadline set'} +
+
+
+

@@ -157,14 +172,19 @@ const ModuleIssueDetailsPage = () => {

)} {(type === 'program' || type === 'module') && ( -
- {tags?.length > 0 && ( - } - isDisabled={true} - /> - )} - {domains?.length > 0 && ( - } - isDisabled={true} - /> + <> +
+ {tags?.length > 0 && ( + } + isDisabled={true} + /> + )} + {domains?.length > 0 && ( + } + isDisabled={true} + /> + )} +
+ {labels?.length > 0 && ( +
+ } + isDisabled={true} + /> +
)} -
+ )} {topContributors && ( = ({ label, className = '' }) => { + return ( + + {label} + + ) +} + +interface LabelListProps { + labels: string[] + maxVisible?: number + className?: string +} + +const LabelList: React.FC = ({ labels, maxVisible = 5, className = '' }) => { + if (!labels || labels.length === 0) return null + + const visibleLabels = labels.slice(0, maxVisible) + const remainingCount = labels.length - maxVisible + + return ( +
+ {visibleLabels.map((label, index) => ( +
+ ) +} + +export { LabelList } diff --git a/frontend/src/components/ModuleCard.tsx b/frontend/src/components/ModuleCard.tsx index ba27e478f3..d1cd84ddd1 100644 --- a/frontend/src/components/ModuleCard.tsx +++ b/frontend/src/components/ModuleCard.tsx @@ -13,6 +13,7 @@ import { useState } from 'react' import type { Module } from 'types/mentorship' import { formatDate } from 'utils/dateFormatter' import { TextInfoItem } from 'components/InfoItem' +import { LabelList } from 'components/LabelList' import SingleModuleCard from 'components/SingleModuleCard' import { TruncatedText } from 'components/TruncatedText' @@ -85,6 +86,11 @@ const ModuleItem = ({ details }: { details: Module }) => { label="Duration" value={getSimpleDuration(details.startedAt, details.endedAt)} /> + {details.labels && details.labels.length > 0 && ( +
+ +
+ )}

) } diff --git a/frontend/src/components/ModuleForm.tsx b/frontend/src/components/ModuleForm.tsx index 9635df159b..9b1913f609 100644 --- a/frontend/src/components/ModuleForm.tsx +++ b/frontend/src/components/ModuleForm.tsx @@ -16,6 +16,7 @@ interface ModuleFormProps { endedAt: string domains: string tags: string + labels: string projectId: string projectName: string mentorLogins: string @@ -185,6 +186,20 @@ const ModuleForm = ({ className="w-full rounded-lg border border-gray-600 bg-gray-50 px-4 py-3 text-gray-800 focus:border-[#1D7BD7] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#1D7BD7] dark:bg-gray-800 dark:text-gray-200 dark:focus-visible:ring-[#1D7BD7]" /> +
+ + +
void diff --git a/frontend/src/types/mentorship.ts b/frontend/src/types/mentorship.ts index 7afecf2465..842dca207b 100644 --- a/frontend/src/types/mentorship.ts +++ b/frontend/src/types/mentorship.ts @@ -61,6 +61,7 @@ export type Module = { endedAt: string domains: string[] tags: string[] + labels: string[] } export type ModuleFormData = { @@ -71,6 +72,7 @@ export type ModuleFormData = { endedAt: string domains: string tags: string + labels: string projectName: string projectId: string mentorLogins: string From b358fc26cbd798bab8af5b890761e71ced138e7e Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Sun, 12 Oct 2025 15:52:04 +0530 Subject: [PATCH 15/36] update suggestion added pagination --- .../github/api/internal/nodes/pull_request.py | 2 + backend/apps/github/models/__init__.py | 1 + .../mentorship/api/internal/nodes/module.py | 47 ++++-- .../management/commands/sync_module_issues.py | 6 +- .../unit/components/CardDetailsPage.test.tsx | 1 + .../unit/components/ItemCardList.test.tsx | 1 + .../components/RecentPullRequests.test.tsx | 2 + .../[moduleKey]/issues/[issueId]/page.tsx | 159 ++++++++++-------- .../modules/[moduleKey]/issues/page.tsx | 52 ++++-- frontend/src/components/SingleModuleCard.tsx | 13 ++ frontend/src/server/queries/issueQueries.ts | 2 + frontend/src/server/queries/moduleQueries.ts | 12 +- frontend/src/types/pullRequest.ts | 2 + 13 files changed, 204 insertions(+), 96 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/pull_request.py b/backend/apps/github/api/internal/nodes/pull_request.py index 8ac55e2221..78a2f280b6 100644 --- a/backend/apps/github/api/internal/nodes/pull_request.py +++ b/backend/apps/github/api/internal/nodes/pull_request.py @@ -12,6 +12,8 @@ fields=[ "created_at", "title", + "state", + "merged_at", ], ) class PullRequestNode(strawberry.relay.Node): diff --git a/backend/apps/github/models/__init__.py b/backend/apps/github/models/__init__.py index c604c01013..cd2639726b 100644 --- a/backend/apps/github/models/__init__.py +++ b/backend/apps/github/models/__init__.py @@ -1,6 +1,7 @@ """Github app.""" from .comment import Comment +from .label import Label from .milestone import Milestone from .pull_request import PullRequest from .user import User diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index b8f1a12339..1531e074c0 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -6,6 +6,7 @@ from apps.github.api.internal.nodes.issue import IssueNode from apps.github.api.internal.nodes.user import UserNode +from apps.github.models import Label from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum from apps.mentorship.api.internal.nodes.mentor import MentorNode from apps.mentorship.api.internal.nodes.program import ProgramNode @@ -41,14 +42,40 @@ def project_name(self) -> str | None: return self.project.name if self.project else None @strawberry.field - def issues(self) -> list[IssueNode]: - """Return issues linked to this module.""" - return list( - self.issues.select_related("repository", "author") - .prefetch_related("assignees", "labels") - .order_by("-created_at") + def issues( + self, limit: int = 20, offset: int = 0, label: str | None = None + ) -> list[IssueNode]: + """Return paginated issues linked to this module, optionally filtered by label.""" + queryset = self.issues.select_related("repository", "author").prefetch_related( + "assignees", "labels" + ) + + if label and label != "all": + queryset = queryset.filter(labels__name=label) + + return list(queryset.order_by("-updated_at")[offset : offset + limit]) + + @strawberry.field + def issues_count(self, label: str | None = None) -> int: + """Return total count of issues linked to this module, optionally filtered by label.""" + queryset = self.issues + + if label and label != "all": + queryset = queryset.filter(labels__name=label) + + return queryset.count() + + @strawberry.field + def available_labels(self) -> list[str]: + """Return all unique labels from issues linked to this module.""" + label_names = ( + Label.objects.filter(issue__mentorship_modules=self) + .values_list("name", flat=True) + .distinct() ) + return sorted(label_names) + @strawberry.field def issue_by_number(self, number: int) -> IssueNode | None: """Return a single issue by its GitHub number within this module's linked issues.""" @@ -74,28 +101,28 @@ def interested_users(self, issue_number: int) -> list[UserNode]: @strawberry.field def task_deadline(self, issue_number: int) -> datetime | None: - """Return the earliest deadline for tasks linked to this module and issue number.""" + """Return the deadline for the latest assigned task linked to this module and issue.""" return ( Task.objects.filter( module=self, issue__number=issue_number, deadline_at__isnull=False, ) - .order_by("deadline_at") + .order_by("-assigned_at") .values_list("deadline_at", flat=True) .first() ) @strawberry.field def task_assigned_at(self, issue_number: int) -> datetime | None: - """Return the earliest assignment time for tasks linked to this module and issue number.""" + """Return the latest assignment time for tasks linked to this module and issue number.""" return ( Task.objects.filter( module=self, issue__number=issue_number, assigned_at__isnull=False, ) - .order_by("assigned_at") + .order_by("-assigned_at") .values_list("assigned_at", flat=True) .first() ) diff --git a/backend/apps/mentorship/management/commands/sync_module_issues.py b/backend/apps/mentorship/management/commands/sync_module_issues.py index 9a19fd6b6f..8e9e9cd587 100644 --- a/backend/apps/mentorship/management/commands/sync_module_issues.py +++ b/backend/apps/mentorship/management/commands/sync_module_issues.py @@ -143,8 +143,7 @@ def _process_module( if task.status != status: updates["status"] = status - # Only fetch assigned_at when needed. - if (created or task.assigned_at is None) and issue.repository: + if issue.repository: repo_full_name = self._extract_repo_full_name(issue.repository) if repo_full_name: if repo_full_name not in repo_cache: @@ -166,6 +165,9 @@ def _process_module( ) if assigned_date: updates["assigned_at"] = assigned_date + self.stdout.write( + f"Updated assignment date for issue #{issue.number}" + ) if created: num_tasks_created += 1 diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx index 5c3e5fd78f..c39fd2f674 100644 --- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx +++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx @@ -524,6 +524,7 @@ describe('CardDetailsPage', () => { organizationName: 'test-org', title: 'Add new feature', url: 'https://github.com/test/project/pull/456', + state: 'open', }, ] diff --git a/frontend/__tests__/unit/components/ItemCardList.test.tsx b/frontend/__tests__/unit/components/ItemCardList.test.tsx index 8f72449bea..8fef9c05b2 100644 --- a/frontend/__tests__/unit/components/ItemCardList.test.tsx +++ b/frontend/__tests__/unit/components/ItemCardList.test.tsx @@ -162,6 +162,7 @@ const mockPullRequest: PullRequest = { repositoryName: 'test-repo', title: 'Add new feature', url: 'https://github.com/test-org/test-repo/pull/456', + state: 'open', } const mockRelease: Release = { diff --git a/frontend/__tests__/unit/components/RecentPullRequests.test.tsx b/frontend/__tests__/unit/components/RecentPullRequests.test.tsx index 14e78fc55a..dd7804a69c 100644 --- a/frontend/__tests__/unit/components/RecentPullRequests.test.tsx +++ b/frontend/__tests__/unit/components/RecentPullRequests.test.tsx @@ -47,6 +47,7 @@ const minimalData = [ repositoryName: 'test-repo', title: 'Test Pull Request', url: 'https://github.com/test-org/test-repo/pull/1', + state: 'open', }, ] @@ -59,6 +60,7 @@ const noRepoData = [ repositoryName: undefined, title: 'Test Pull Request', url: 'https://github.com/test-org/test-repo/pull/2', + state: 'open', }, ] diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 40304d9844..1e595514c0 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -22,6 +22,7 @@ import { import ActionButton from 'components/ActionButton' import AnchorTitle from 'components/AnchorTitle' import LoadingSpinner from 'components/LoadingSpinner' +import Markdown from 'components/MarkdownWrapper' import SecondaryCard from 'components/SecondaryCard' import { TruncatedText } from 'components/TruncatedText' @@ -83,10 +84,10 @@ const ModuleIssueDetailsPage = () => { return (
-
-
-

- +
+
+

+ {issue.title}

{issue.organizationName}/{issue.repositoryName} • #{issue.number} @@ -98,8 +99,8 @@ const ModuleIssueDetailsPage = () => {
}> -
- {issue.summary || 'No description.'} +
+
@@ -141,63 +142,64 @@ const ModuleIssueDetailsPage = () => {
-
-

-
-
- + {assignees.length > 0 && ( +
+

+
+
+ +
+ Assignees
- Assignees -

-

-
- {assignees.map((a) => ( -
- - {a.avatarUrl ? ( - {a.login} - ) : ( -

+
+ {assignees.map((a) => ( +
- - -
- ))} - {assignees.length === 0 && Unassigned} + + {a.avatarUrl ? ( + {a.login} + ) : ( + + ))} +
-
+ )}
@@ -207,27 +209,29 @@ const ModuleIssueDetailsPage = () => { key={pr.id} className="flex items-center justify-between gap-3 rounded-lg bg-gray-200 p-4 dark:bg-gray-700" > -
+
{pr.author?.avatarUrl ? ( {pr.author?.login ) : ( - )) ) : ( diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 5f7a81b60f..302e51b1b8 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -9,18 +9,28 @@ import { ErrorDisplay, handleAppError } from 'app/global-error' import { GET_MODULE_ISSUES } from 'server/queries/moduleQueries' import type { Issue } from 'types/issue' import LoadingSpinner from 'components/LoadingSpinner' +import Pagination from 'components/Pagination' import { TruncatedText } from 'components/TruncatedText' const LABEL_ALL = 'all' +const ITEMS_PER_PAGE = 20 +const MAX_VISIBLE_LABELS = 5 const IssuesPage = () => { const { programKey, moduleKey } = useParams() as { programKey: string; moduleKey: string } const router = useRouter() const searchParams = useSearchParams() const [selectedLabel, setSelectedLabel] = useState(searchParams.get('label') || LABEL_ALL) + const [currentPage, setCurrentPage] = useState(1) const { data, loading, error } = useQuery(GET_MODULE_ISSUES, { - variables: { programKey, moduleKey }, + variables: { + programKey, + moduleKey, + limit: ITEMS_PER_PAGE, + offset: (currentPage - 1) * ITEMS_PER_PAGE, + label: selectedLabel === LABEL_ALL ? null : selectedLabel, + }, skip: !programKey || !moduleKey, fetchPolicy: 'cache-and-network', }) @@ -31,7 +41,7 @@ const IssuesPage = () => { const moduleData = data?.getModule const moduleIssues: Issue[] = useMemo(() => { - const issues = (moduleData?.issues || []).map((i) => ({ + return (moduleData?.issues || []).map((i) => ({ author: i.author, createdAt: i.createdAt, hint: '', @@ -48,11 +58,16 @@ const IssuesPage = () => { url: i.url, objectID: i.id, })) - if (selectedLabel === LABEL_ALL) return issues - return issues.filter((iss) => (iss.labels || []).includes(selectedLabel)) - }, [moduleData, selectedLabel]) + }, [moduleData]) + + const totalPages = Math.ceil((moduleData?.issuesCount || 0) / ITEMS_PER_PAGE) const allLabels: string[] = useMemo(() => { + const serverLabels = moduleData?.availableLabels + if (serverLabels && serverLabels.length > 0) { + return serverLabels + } + const labels = new Set() ;(moduleData?.issues || []).forEach((i) => (i.labels || []).forEach((l: string) => labels.add(l)) @@ -62,6 +77,7 @@ const IssuesPage = () => { const handleLabelChange = (label: string) => { setSelectedLabel(label) + setCurrentPage(1) const params = new URLSearchParams(searchParams.toString()) if (label === LABEL_ALL) { params.delete('label') @@ -71,6 +87,14 @@ const IssuesPage = () => { router.replace(`?${params.toString()}`) } + const handlePageChange = (page: number) => { + setCurrentPage(page) + } + + const handleIssueClick = (issueNumber: number) => { + router.push(`/my/mentorship/programs/${programKey}/modules/${moduleKey}/issues/${issueNumber}`) + } + if (loading) return if (!moduleData) return @@ -142,12 +166,8 @@ const IssuesPage = () => {
) diff --git a/frontend/src/components/SingleModuleCard.tsx b/frontend/src/components/SingleModuleCard.tsx index ade79a3e8d..2cb2f63a51 100644 --- a/frontend/src/components/SingleModuleCard.tsx +++ b/frontend/src/components/SingleModuleCard.tsx @@ -45,6 +45,11 @@ const SingleModuleCard: React.FC = ({ router.push(`${window.location.pathname}/modules/create`) } + const handleIssue = () => { + setDropdownOpen(false) + router.push(`${window.location.pathname}/modules/${module.key}/issues`) + } + const moduleDetails = [ { label: 'Experience Level', value: upperFirst(module.experienceLevel) }, { label: 'Start Date', value: formatDate(module.startedAt) }, @@ -111,6 +116,14 @@ const SingleModuleCard: React.FC = ({ Create Module )} + {isAdmin && ( + + )}
)}
diff --git a/frontend/src/server/queries/issueQueries.ts b/frontend/src/server/queries/issueQueries.ts index e1abd34661..e1e49f4d9f 100644 --- a/frontend/src/server/queries/issueQueries.ts +++ b/frontend/src/server/queries/issueQueries.ts @@ -33,7 +33,9 @@ export const GET_MODULE_ISSUE_VIEW = gql` id title url + state createdAt + mergedAt author { id login diff --git a/frontend/src/server/queries/moduleQueries.ts b/frontend/src/server/queries/moduleQueries.ts index 86bfdb3c7b..3a1d0353aa 100644 --- a/frontend/src/server/queries/moduleQueries.ts +++ b/frontend/src/server/queries/moduleQueries.ts @@ -78,12 +78,20 @@ export const GET_PROGRAM_ADMINS_AND_MODULES = gql` ` export const GET_MODULE_ISSUES = gql` - query GetModuleIssues($programKey: String!, $moduleKey: String!) { + query GetModuleIssues( + $programKey: String! + $moduleKey: String! + $limit: Int = 20 + $offset: Int = 0 + $label: String + ) { getModule(moduleKey: $moduleKey, programKey: $programKey) { id name key - issues { + issuesCount(label: $label) + availableLabels + issues(limit: $limit, offset: $offset, label: $label) { id number createdAt diff --git a/frontend/src/types/pullRequest.ts b/frontend/src/types/pullRequest.ts index ccd796587c..b0980f3f1b 100644 --- a/frontend/src/types/pullRequest.ts +++ b/frontend/src/types/pullRequest.ts @@ -8,4 +8,6 @@ export type PullRequest = { repositoryName?: string title: string url: string + state: string + mergedAt?: string } From ede2bc7e1c752ea821917826b24322002d886eb3 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Sun, 12 Oct 2025 16:09:14 +0530 Subject: [PATCH 16/36] fix backend test case --- .../tests/apps/github/api/internal/nodes/pull_request_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/tests/apps/github/api/internal/nodes/pull_request_test.py b/backend/tests/apps/github/api/internal/nodes/pull_request_test.py index 90d54cdcff..e5cad0f0a0 100644 --- a/backend/tests/apps/github/api/internal/nodes/pull_request_test.py +++ b/backend/tests/apps/github/api/internal/nodes/pull_request_test.py @@ -17,8 +17,10 @@ def test_meta_configuration(self): "_id", "author", "created_at", + "merged_at", "organization_name", "repository_name", + "state", "title", "url", } From fb70d241c6160460e2c7def495aea9e9a5b40e52 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Sun, 12 Oct 2025 16:28:50 +0530 Subject: [PATCH 17/36] update mock data --- frontend/__tests__/unit/components/CardDetailsPage.test.tsx | 1 + frontend/__tests__/unit/components/ItemCardList.test.tsx | 1 + frontend/__tests__/unit/components/RecentPullRequests.test.tsx | 2 ++ 3 files changed, 4 insertions(+) diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx index c39fd2f674..4a33af8c22 100644 --- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx +++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx @@ -525,6 +525,7 @@ describe('CardDetailsPage', () => { title: 'Add new feature', url: 'https://github.com/test/project/pull/456', state: 'open', + mergedAt: new Date(Date.now() - 86400000).toISOString(), }, ] diff --git a/frontend/__tests__/unit/components/ItemCardList.test.tsx b/frontend/__tests__/unit/components/ItemCardList.test.tsx index 8fef9c05b2..09e7767a4c 100644 --- a/frontend/__tests__/unit/components/ItemCardList.test.tsx +++ b/frontend/__tests__/unit/components/ItemCardList.test.tsx @@ -163,6 +163,7 @@ const mockPullRequest: PullRequest = { title: 'Add new feature', url: 'https://github.com/test-org/test-repo/pull/456', state: 'open', + mergedAt: '2022-01-02T00:00:00Z', } const mockRelease: Release = { diff --git a/frontend/__tests__/unit/components/RecentPullRequests.test.tsx b/frontend/__tests__/unit/components/RecentPullRequests.test.tsx index dd7804a69c..02d029d383 100644 --- a/frontend/__tests__/unit/components/RecentPullRequests.test.tsx +++ b/frontend/__tests__/unit/components/RecentPullRequests.test.tsx @@ -48,6 +48,7 @@ const minimalData = [ title: 'Test Pull Request', url: 'https://github.com/test-org/test-repo/pull/1', state: 'open', + mergedAt: '2024-06-02T12:00:00Z', }, ] @@ -61,6 +62,7 @@ const noRepoData = [ title: 'Test Pull Request', url: 'https://github.com/test-org/test-repo/pull/2', state: 'open', + mergedAt: '2024-06-02T12:00:00Z', }, ] From 6e070a99c961c4bf62fb48046d54707795890f60 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 15 Oct 2025 02:45:18 +0530 Subject: [PATCH 18/36] update code and added queries --- backend/.env.example | 22 ------ backend/apps/github/Makefile | 2 +- .../apps/github/api/internal/nodes/issue.py | 2 +- .../unit/components/CardDetailsPage.test.tsx | 4 +- .../[moduleKey]/issues/[issueId]/page.tsx | 25 ++++--- .../modules/[moduleKey]/issues/page.tsx | 51 ++++++++++---- frontend/src/server/queries/issueQueries.ts | 9 +-- frontend/src/server/queries/moduleQueries.ts | 27 +------- frontend/src/types/__generated__/graphql.ts | 68 +++++++++++++++++++ .../__generated__/issueQueries.generated.ts | 36 ++++++++++ .../moduleMutations.generated.ts | 8 +-- .../__generated__/moduleQueries.generated.ts | 16 ++++- .../programsQueries.generated.ts | 4 +- frontend/src/types/issue.ts | 2 +- 14 files changed, 185 insertions(+), 91 deletions(-) delete mode 100644 backend/.env.example create mode 100644 frontend/src/types/__generated__/issueQueries.generated.ts diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 2916e696d1..0000000000 --- a/backend/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -DJANGO_ALGOLIA_APPLICATION_ID=None -DJANGO_ALGOLIA_EXCLUDED_LOCAL_INDEX_NAMES=None -DJANGO_ALGOLIA_WRITE_API_KEY=None -DJANGO_ALLOWED_HOSTS=* -DJANGO_AWS_ACCESS_KEY_ID=None -DJANGO_AWS_SECRET_ACCESS_KEY=None -DJANGO_CONFIGURATION=Test -DJANGO_DB_HOST=None -DJANGO_DB_NAME=None -DJANGO_DB_PASSWORD=None -DJANGO_DB_PORT=None -DJANGO_DB_USER=None -DJANGO_OPEN_AI_SECRET_KEY=None -DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1" -DJANGO_REDIS_HOST=None -DJANGO_REDIS_PASSWORD=None -DJANGO_RELEASE_VERSION=None -DJANGO_SECRET_KEY=None -DJANGO_SENTRY_DSN=None -DJANGO_SLACK_BOT_TOKEN=None -DJANGO_SLACK_SIGNING_SECRET=None -GITHUB_TOKEN=None diff --git a/backend/apps/github/Makefile b/backend/apps/github/Makefile index e0ea5e7def..d2bceff20b 100644 --- a/backend/apps/github/Makefile +++ b/backend/apps/github/Makefile @@ -5,7 +5,7 @@ github-add-related-repositories: github-enrich-issues: @echo "Enriching GitHub issues" @CMD="python manage.py github_enrich_issues" $(MAKE) exec-backend-command - + github-update-owasp-organization: @echo "Updating OWASP GitHub organization" @CMD="python manage.py github_update_owasp_organization" $(MAKE) exec-backend-command diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 1b4d89d232..9f2b203db7 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -15,7 +15,7 @@ "created_at", "number", "state", - "summary", + "body", "title", "url", ], diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx index 4a33af8c22..eb6d6576f4 100644 --- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx +++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx @@ -494,7 +494,7 @@ describe('CardDetailsPage', () => { organizationName: 'test-org', projectName: 'Test Project', projectUrl: 'https://github.com/test/project', - summary: 'Issue summary', + body: 'Issue summary', title: 'Test Issue', updatedAt: Date.now(), url: 'https://github.com/test/project/issues/123', @@ -524,7 +524,7 @@ describe('CardDetailsPage', () => { organizationName: 'test-org', title: 'Add new feature', url: 'https://github.com/test/project/pull/456', - state: 'open', + state: 'merged', mergedAt: new Date(Date.now() - 86400000).toISOString(), }, ] diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 1e595514c0..c5bc959047 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -89,8 +89,19 @@ const ModuleIssueDetailsPage = () => {

{issue.title}

-
- {issue.organizationName}/{issue.repositoryName} • #{issue.number} +
+ + {issue.organizationName}/{issue.repositoryName} • #{issue.number} + + + {issue.state === 'open' ? 'Open' : 'Closed'} +
@@ -100,7 +111,7 @@ const ModuleIssueDetailsPage = () => { }>
- +
@@ -241,7 +252,7 @@ const ModuleIssueDetailsPage = () => {
{pr.state === 'closed' && pr.mergedAt ? ( - + Merged ) : pr.state === 'closed' ? ( @@ -249,14 +260,10 @@ const ModuleIssueDetailsPage = () => { Closed ) : ( - + Open )} - - - View PR -
)) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 302e51b1b8..e43fd96d58 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -2,6 +2,7 @@ import { useQuery } from '@apollo/client' import { Select, SelectItem } from '@heroui/select' +import { Tooltip } from '@heroui/tooltip' import Image from 'next/image' import { useParams, useRouter, useSearchParams } from 'next/navigation' import { useEffect, useMemo, useState } from 'react' @@ -10,7 +11,6 @@ import { GET_MODULE_ISSUES } from 'server/queries/moduleQueries' import type { Issue } from 'types/issue' import LoadingSpinner from 'components/LoadingSpinner' import Pagination from 'components/Pagination' -import { TruncatedText } from 'components/TruncatedText' const LABEL_ALL = 'all' const ITEMS_PER_PAGE = 20 @@ -52,8 +52,9 @@ const IssuesPage = () => { projectUrl: '', repository: undefined, repositoryLanguages: [], - summary: '', + body: i.body || '', title: i.title, + state: i.state, updatedAt: i.createdAt, url: i.url, objectID: i.id, @@ -146,6 +147,12 @@ const IssuesPage = () => { > Title + + Status + { {moduleIssues.map((issue) => ( - + + + + - 50 ? '…' : ''}`} - /> - + {issue.state === 'open' ? 'Open' : 'Closed'} +
@@ -220,9 +245,7 @@ const IssuesPage = () => {
)} - ) : ( - Unassigned - ))( + ) : null)( (data?.getModule?.issues || []).find((i) => i.id === issue.objectID) ?.assignees )} @@ -232,7 +255,7 @@ const IssuesPage = () => { {moduleIssues.length === 0 && ( No issues found for the selected filter. diff --git a/frontend/src/server/queries/issueQueries.ts b/frontend/src/server/queries/issueQueries.ts index e1e49f4d9f..9038a40649 100644 --- a/frontend/src/server/queries/issueQueries.ts +++ b/frontend/src/server/queries/issueQueries.ts @@ -10,18 +10,11 @@ export const GET_MODULE_ISSUE_VIEW = gql` id number title - summary + body url state - createdAt organizationName repositoryName - author { - id - login - name - avatarUrl - } assignees { id login diff --git a/frontend/src/server/queries/moduleQueries.ts b/frontend/src/server/queries/moduleQueries.ts index 3a1d0353aa..323f2d813b 100644 --- a/frontend/src/server/queries/moduleQueries.ts +++ b/frontend/src/server/queries/moduleQueries.ts @@ -86,43 +86,20 @@ export const GET_MODULE_ISSUES = gql` $label: String ) { getModule(moduleKey: $moduleKey, programKey: $programKey) { - id name - key issuesCount(label: $label) availableLabels issues(limit: $limit, offset: $offset, label: $label) { id number - createdAt title - summary - url - author { - id - avatarUrl - login - name - } + state + labels assignees { - id avatarUrl login name } - labels - pullRequests { - id - title - url - createdAt - author { - id - login - name - avatarUrl - } - } } } } diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts index 17f2ff0c06..adb2c61067 100644 --- a/frontend/src/types/__generated__/graphql.ts +++ b/frontend/src/types/__generated__/graphql.ts @@ -102,6 +102,7 @@ export type CreateModuleInput = { domains?: Array; endedAt: Scalars['DateTime']['input']; experienceLevel: ExperienceLevelEnum; + labels?: Array; mentorLogins?: InputMaybe>; name: Scalars['String']['input']; programKey: Scalars['String']['input']; @@ -182,11 +183,17 @@ export type GitHubAuthResult = { export type IssueNode = Node & { __typename?: 'IssueNode'; + assignees: Array; author?: Maybe; + body: Scalars['String']['output']; createdAt: Scalars['DateTime']['output']; /** The Globally Unique ID of this object */ id: Scalars['ID']['output']; + interestedUsers: Array; + labels: Array; + number: Scalars['Int']['output']; organizationName?: Maybe; + pullRequests: Array; repositoryName?: Maybe; state: Scalars['String']['output']; title: Scalars['String']['output']; @@ -227,12 +234,18 @@ export type MilestoneNode = Node & { export type ModuleNode = { __typename?: 'ModuleNode'; + availableLabels: Array; description: Scalars['String']['output']; domains?: Maybe>; endedAt: Scalars['DateTime']['output']; experienceLevel: ExperienceLevelEnum; id: Scalars['ID']['output']; + interestedUsers: Array; + issueByNumber?: Maybe; + issues: Array; + issuesCount: Scalars['Int']['output']; key: Scalars['String']['output']; + labels?: Maybe>; mentors: Array; name: Scalars['String']['output']; program?: Maybe; @@ -240,22 +253,66 @@ export type ModuleNode = { projectName?: Maybe; startedAt: Scalars['DateTime']['output']; tags?: Maybe>; + taskAssignedAt?: Maybe; + taskDeadline?: Maybe; +}; + + +export type ModuleNodeInterestedUsersArgs = { + issueNumber: Scalars['Int']['input']; +}; + + +export type ModuleNodeIssueByNumberArgs = { + number: Scalars['Int']['input']; +}; + + +export type ModuleNodeIssuesArgs = { + label?: InputMaybe; + limit?: Scalars['Int']['input']; + offset?: Scalars['Int']['input']; +}; + + +export type ModuleNodeIssuesCountArgs = { + label?: InputMaybe; +}; + + +export type ModuleNodeTaskAssignedAtArgs = { + issueNumber: Scalars['Int']['input']; +}; + + +export type ModuleNodeTaskDeadlineArgs = { + issueNumber: Scalars['Int']['input']; }; export type Mutation = { __typename?: 'Mutation'; + assignIssueToUser: ModuleNode; createApiKey: CreateApiKeyResult; createModule: ModuleNode; createProgram: ProgramNode; githubAuth: GitHubAuthResult; logoutUser: LogoutResult; revokeApiKey: RevokeApiKeyResult; + unassignIssueFromUser: ModuleNode; updateModule: ModuleNode; updateProgram: ProgramNode; updateProgramStatus: ProgramNode; }; +export type MutationAssignIssueToUserArgs = { + issueNumber: Scalars['Int']['input']; + moduleKey: Scalars['String']['input']; + programKey: Scalars['String']['input']; + userLogin: Scalars['String']['input']; +}; + + export type MutationCreateApiKeyArgs = { expiresAt: Scalars['DateTime']['input']; name: Scalars['String']['input']; @@ -282,6 +339,14 @@ export type MutationRevokeApiKeyArgs = { }; +export type MutationUnassignIssueFromUserArgs = { + issueNumber: Scalars['Int']['input']; + moduleKey: Scalars['String']['input']; + programKey: Scalars['String']['input']; + userLogin: Scalars['String']['input']; +}; + + export type MutationUpdateModuleArgs = { inputData: UpdateModuleInput; }; @@ -495,8 +560,10 @@ export type PullRequestNode = Node & { createdAt: Scalars['DateTime']['output']; /** The Globally Unique ID of this object */ id: Scalars['ID']['output']; + mergedAt?: Maybe; organizationName?: Maybe; repositoryName?: Maybe; + state: Scalars['String']['output']; title: Scalars['String']['output']; url: Scalars['String']['output']; }; @@ -828,6 +895,7 @@ export type UpdateModuleInput = { endedAt: Scalars['DateTime']['input']; experienceLevel: ExperienceLevelEnum; key: Scalars['String']['input']; + labels?: Array; mentorLogins?: InputMaybe>; name: Scalars['String']['input']; programKey: Scalars['String']['input']; diff --git a/frontend/src/types/__generated__/issueQueries.generated.ts b/frontend/src/types/__generated__/issueQueries.generated.ts new file mode 100644 index 0000000000..927c67dda9 --- /dev/null +++ b/frontend/src/types/__generated__/issueQueries.generated.ts @@ -0,0 +1,36 @@ +import * as Types from './graphql'; + +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type GetModuleIssueViewQueryVariables = Types.Exact<{ + programKey: Types.Scalars['String']['input']; + moduleKey: Types.Scalars['String']['input']; + number: Types.Scalars['Int']['input']; +}>; + + +export type GetModuleIssueViewQuery = { getModule: { __typename: 'ModuleNode', id: string, taskDeadline: unknown | null, taskAssignedAt: unknown | null, issueByNumber: { __typename: 'IssueNode', id: string, number: number, title: string, body: string, url: string, state: string, organizationName: string | null, repositoryName: string | null, labels: Array, assignees: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }>, pullRequests: Array<{ __typename: 'PullRequestNode', id: string, title: string, url: string, state: string, createdAt: unknown, mergedAt: unknown | null, author: { __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string } | null }> } | null, interestedUsers: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }> } }; + +export type AssignIssueToUserMutationVariables = Types.Exact<{ + programKey: Types.Scalars['String']['input']; + moduleKey: Types.Scalars['String']['input']; + issueNumber: Types.Scalars['Int']['input']; + userLogin: Types.Scalars['String']['input']; +}>; + + +export type AssignIssueToUserMutation = { assignIssueToUser: { __typename: 'ModuleNode', id: string } }; + +export type UnassignIssueFromUserMutationVariables = Types.Exact<{ + programKey: Types.Scalars['String']['input']; + moduleKey: Types.Scalars['String']['input']; + issueNumber: Types.Scalars['Int']['input']; + userLogin: Types.Scalars['String']['input']; +}>; + + +export type UnassignIssueFromUserMutation = { unassignIssueFromUser: { __typename: 'ModuleNode', id: string } }; + + +export const GetModuleIssueViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleIssueView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"number"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"taskDeadline"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}]},{"kind":"Field","name":{"kind":"Name","value":"taskAssignedAt"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}]},{"kind":"Field","name":{"kind":"Name","value":"issueByNumber"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"number"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"pullRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"mergedAt"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"interestedUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; +export const AssignIssueToUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AssignIssueToUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignIssueToUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}}},{"kind":"Argument","name":{"kind":"Name","value":"userLogin"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; +export const UnassignIssueFromUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnassignIssueFromUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unassignIssueFromUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}}},{"kind":"Argument","name":{"kind":"Name","value":"userLogin"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/moduleMutations.generated.ts b/frontend/src/types/__generated__/moduleMutations.generated.ts index c3d8ee0463..8ae7ff275e 100644 --- a/frontend/src/types/__generated__/moduleMutations.generated.ts +++ b/frontend/src/types/__generated__/moduleMutations.generated.ts @@ -6,15 +6,15 @@ export type UpdateModuleMutationVariables = Types.Exact<{ }>; -export type UpdateModuleMutation = { updateModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, tags: Array | null, domains: Array | null, projectId: string | null, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> } }; +export type UpdateModuleMutation = { updateModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, tags: Array | null, domains: Array | null, labels: Array | null, projectId: string | null, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> } }; export type CreateModuleMutationVariables = Types.Exact<{ input: Types.CreateModuleInput; }>; -export type CreateModuleMutation = { createModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, domains: Array | null, tags: Array | null, projectId: string | null, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> } }; +export type CreateModuleMutation = { createModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, domains: Array | null, tags: Array | null, labels: Array | null, projectId: string | null, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> } }; -export const UpdateModuleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateModule"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateModuleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inputData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreateModuleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateModule"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateModuleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inputData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const UpdateModuleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateModule"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateModuleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inputData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateModuleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateModule"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateModuleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inputData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/moduleQueries.generated.ts b/frontend/src/types/__generated__/moduleQueries.generated.ts index 50d5bbc5bc..ad2133551b 100644 --- a/frontend/src/types/__generated__/moduleQueries.generated.ts +++ b/frontend/src/types/__generated__/moduleQueries.generated.ts @@ -22,9 +22,21 @@ export type GetProgramAdminsAndModulesQueryVariables = Types.Exact<{ }>; -export type GetProgramAdminsAndModulesQuery = { getProgram: { __typename: 'ProgramNode', id: string, admins: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> | null }, getModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, tags: Array | null, projectId: string | null, projectName: string | null, domains: Array | null, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> } }; +export type GetProgramAdminsAndModulesQuery = { getProgram: { __typename: 'ProgramNode', id: string, admins: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> | null }, getModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, tags: Array | null, labels: Array | null, projectId: string | null, projectName: string | null, domains: Array | null, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> } }; + +export type GetModuleIssuesQueryVariables = Types.Exact<{ + programKey: Types.Scalars['String']['input']; + moduleKey: Types.Scalars['String']['input']; + limit?: Types.InputMaybe; + offset?: Types.InputMaybe; + label?: Types.InputMaybe; +}>; + + +export type GetModuleIssuesQuery = { getModule: { __typename: 'ModuleNode', name: string, issuesCount: number, availableLabels: Array, issues: Array<{ __typename: 'IssueNode', id: string, number: number, title: string, state: string, labels: Array, assignees: Array<{ __typename: 'UserNode', avatarUrl: string, login: string, name: string }> }> } }; export const GetModulesByProgramDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModulesByProgram"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgramModules"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetModuleByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleByID"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetProgramAdminsAndModulesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProgramAdminsAndModules"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgram"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const GetProgramAdminsAndModulesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProgramAdminsAndModules"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgram"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetModuleIssuesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleIssues"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"label"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}]},{"kind":"Field","name":{"kind":"Name","value":"availableLabels"}},{"kind":"Field","name":{"kind":"Name","value":"issues"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/programsQueries.generated.ts b/frontend/src/types/__generated__/programsQueries.generated.ts index daa4783da8..bcbe8e9058 100644 --- a/frontend/src/types/__generated__/programsQueries.generated.ts +++ b/frontend/src/types/__generated__/programsQueries.generated.ts @@ -8,7 +8,7 @@ export type GetMyProgramsQueryVariables = Types.Exact<{ }>; -export type GetMyProgramsQuery = { myPrograms: { __typename: 'PaginatedPrograms', currentPage: number, totalPages: number, programs: Array<{ __typename: 'ProgramNode', id: string, key: string, name: string, description: string, startedAt: unknown, endedAt: unknown, userRole: string | null }> } }; +export type GetMyProgramsQuery = { myPrograms: { __typename: 'PaginatedPrograms', currentPage: number, totalPages: number, programs: Array<{ __typename: 'ProgramNode', id: string, key: string, name: string, status: Types.ProgramStatusEnum, description: string, startedAt: unknown, endedAt: unknown, userRole: string | null }> } }; export type GetProgramDetailsQueryVariables = Types.Exact<{ programKey: Types.Scalars['String']['input']; @@ -32,7 +32,7 @@ export type GetProgramAdminDetailsQueryVariables = Types.Exact<{ export type GetProgramAdminDetailsQuery = { getProgram: { __typename: 'ProgramNode', id: string, key: string, name: string, admins: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> | null } }; -export const GetMyProgramsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMyPrograms"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myPrograms"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentPage"}},{"kind":"Field","name":{"kind":"Name","value":"totalPages"}},{"kind":"Field","name":{"kind":"Name","value":"programs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"userRole"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetMyProgramsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMyPrograms"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myPrograms"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentPage"}},{"kind":"Field","name":{"kind":"Name","value":"totalPages"}},{"kind":"Field","name":{"kind":"Name","value":"programs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"userRole"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetProgramDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProgramDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgram"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"menteesLimit"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevels"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetProgramAndModulesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProgramAndModules"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgram"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"menteesLimit"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevels"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"getProgramModules"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetProgramAdminDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProgramAdminDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgram"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/issue.ts b/frontend/src/types/issue.ts index 5be98aaf30..56b1568efd 100644 --- a/frontend/src/types/issue.ts +++ b/frontend/src/types/issue.ts @@ -13,7 +13,7 @@ export type Issue = { pullRequests?: PullRequest[] repository?: RepositoryDetails repositoryLanguages?: string[] - summary: string + body: string title: string updatedAt: number url: string From f43cad79d95ee306d0394c97c80d16564ed639c8 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 15 Oct 2025 02:50:43 +0530 Subject: [PATCH 19/36] update formatting --- backend/apps/github/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/github/Makefile b/backend/apps/github/Makefile index d2bceff20b..e0ea5e7def 100644 --- a/backend/apps/github/Makefile +++ b/backend/apps/github/Makefile @@ -5,7 +5,7 @@ github-add-related-repositories: github-enrich-issues: @echo "Enriching GitHub issues" @CMD="python manage.py github_enrich_issues" $(MAKE) exec-backend-command - + github-update-owasp-organization: @echo "Updating OWASP GitHub organization" @CMD="python manage.py github_update_owasp_organization" $(MAKE) exec-backend-command From 0851ebf5f3eee14c054cc3a469d3325a54e5cc1e Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 15 Oct 2025 03:07:33 +0530 Subject: [PATCH 20/36] revert changes and update test cases --- backend/.env.example | 22 +++++++++++++++++++ backend/manage.py | 3 +++ .../github/api/internal/nodes/issue_test.py | 2 +- frontend/src/types/issue.ts | 4 +++- 4 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 backend/.env.example diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000000..2916e696d1 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,22 @@ +DJANGO_ALGOLIA_APPLICATION_ID=None +DJANGO_ALGOLIA_EXCLUDED_LOCAL_INDEX_NAMES=None +DJANGO_ALGOLIA_WRITE_API_KEY=None +DJANGO_ALLOWED_HOSTS=* +DJANGO_AWS_ACCESS_KEY_ID=None +DJANGO_AWS_SECRET_ACCESS_KEY=None +DJANGO_CONFIGURATION=Test +DJANGO_DB_HOST=None +DJANGO_DB_NAME=None +DJANGO_DB_PASSWORD=None +DJANGO_DB_PORT=None +DJANGO_DB_USER=None +DJANGO_OPEN_AI_SECRET_KEY=None +DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1" +DJANGO_REDIS_HOST=None +DJANGO_REDIS_PASSWORD=None +DJANGO_RELEASE_VERSION=None +DJANGO_SECRET_KEY=None +DJANGO_SENTRY_DSN=None +DJANGO_SLACK_BOT_TOKEN=None +DJANGO_SLACK_SIGNING_SECRET=None +GITHUB_TOKEN=None diff --git a/backend/manage.py b/backend/manage.py index 83d4673775..56bdab97cc 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -5,7 +5,10 @@ import os import sys +from dotenv import load_dotenv + if __name__ == "__main__": + load_dotenv() os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.local") os.environ.setdefault("DJANGO_CONFIGURATION", "Local") diff --git a/backend/tests/apps/github/api/internal/nodes/issue_test.py b/backend/tests/apps/github/api/internal/nodes/issue_test.py index fdfb5aad98..b0ed33467e 100644 --- a/backend/tests/apps/github/api/internal/nodes/issue_test.py +++ b/backend/tests/apps/github/api/internal/nodes/issue_test.py @@ -18,7 +18,7 @@ def test_issue_node_fields(self): "created_at", "number", "state", - "summary", + "body", "title", "url", "author", diff --git a/frontend/src/types/issue.ts b/frontend/src/types/issue.ts index 56b1568efd..25c83f68bf 100644 --- a/frontend/src/types/issue.ts +++ b/frontend/src/types/issue.ts @@ -13,8 +13,10 @@ export type Issue = { pullRequests?: PullRequest[] repository?: RepositoryDetails repositoryLanguages?: string[] - body: string + body?: string title: string + state?: string + summary?: string updatedAt: number url: string objectID: string From 9400c376f4e9c57545581c7559e62e43a035ed35 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 15 Oct 2025 03:39:36 +0530 Subject: [PATCH 21/36] update component better error handling --- .../[moduleKey]/issues/[issueId]/page.tsx | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index c5bc959047..a31f765564 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -10,6 +10,7 @@ import { faXmark, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { addToast } from '@heroui/toast' import Image from 'next/image' import Link from 'next/link' import { useParams } from 'next/navigation' @@ -34,7 +35,7 @@ const ModuleIssueDetailsPage = () => { moduleKey: string issueId: string } - const { data, loading } = useQuery(GET_MODULE_ISSUE_VIEW, { + const { data, loading, error } = useQuery(GET_MODULE_ISSUE_VIEW, { variables: { programKey, moduleKey, number: Number(issueId) }, skip: !issueId, fetchPolicy: 'cache-first', @@ -49,6 +50,24 @@ const ModuleIssueDetailsPage = () => { }, ], awaitRefetchQueries: true, + onCompleted: () => { + addToast({ + title: 'Issue assigned successfully', + variant: 'solid', + color: 'success', + timeout: 3000, + shouldShowTimeoutProgress: true, + }) + }, + onError: (error) => { + addToast({ + title: 'Failed to assign issue: ' + error.message, + variant: 'solid', + color: 'danger', + timeout: 3000, + shouldShowTimeoutProgress: true, + }) + }, }) const [unassignIssue, { loading: unassigning }] = useMutation(UNASSIGN_ISSUE_FROM_USER, { refetchQueries: [ @@ -58,10 +77,31 @@ const ModuleIssueDetailsPage = () => { }, ], awaitRefetchQueries: true, + onCompleted: () => { + addToast({ + title: 'Issue unassigned successfully', + variant: 'solid', + color: 'success', + timeout: 3000, + shouldShowTimeoutProgress: true, + }) + }, + onError: (error) => { + addToast({ + title: 'Failed to unassign issue: ' + error.message, + variant: 'solid', + color: 'danger', + timeout: 3000, + shouldShowTimeoutProgress: true, + }) + }, }) const issue = data?.getModule?.issueByNumber + if (error) { + return + } if (loading) return if (!issue) return @@ -192,14 +232,18 @@ const ModuleIssueDetailsPage = () => { disabled={!issueId || unassigning} onClick={async () => { if (!issueId || unassigning) return - await unassignIssue({ - variables: { - programKey, - moduleKey, - issueNumber: Number(issueId), - userLogin: a.login, - }, - }) + try { + await unassignIssue({ + variables: { + programKey, + moduleKey, + issueNumber: Number(issueId), + userLogin: a.login, + }, + }) + } catch (error) { + throw new Error('Failed to unassign user', error as Error) + } }} className={getButtonClassName(!issueId || unassigning)} title={unassigning ? 'Unassigning…' : `Unassign @${a.login}`} @@ -307,14 +351,18 @@ const ModuleIssueDetailsPage = () => { disabled={!issueId || assigning} onClick={async () => { if (!issueId || assigning) return - await assignIssue({ - variables: { - programKey, - moduleKey, - issueNumber: Number(issueId), - userLogin: u.login, - }, - }) + try { + await assignIssue({ + variables: { + programKey, + moduleKey, + issueNumber: Number(issueId), + userLogin: u.login, + }, + }) + } catch (error) { + throw new Error('Failed to assign user', error as Error) + } }} className={`${getButtonClassName(!issueId || assigning)} px-3 py-1`} title={ From d7de36160f6a917101af655039461daf741ad728 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 15 Oct 2025 03:39:57 +0530 Subject: [PATCH 22/36] remove n+1 query --- backend/apps/github/api/internal/nodes/issue.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 9f2b203db7..1e3d29dfd9 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -6,7 +6,6 @@ from apps.github.api.internal.nodes.pull_request import PullRequestNode from apps.github.api.internal.nodes.user import UserNode from apps.github.models.issue import Issue -from apps.mentorship.models import IssueUserInterest # add import @strawberry_django.type( @@ -55,7 +54,12 @@ def labels(self) -> list[str]: @strawberry.field def interested_users(self) -> list[UserNode]: """Return all users who have expressed interest in this issue.""" - return [interest.user for interest in IssueUserInterest.objects.filter(issue=self)] + return [ + interest.user + for interest in self.participant_interests.select_related("user").order_by( + "user__login" + ) + ] @strawberry.field def pull_requests(self) -> list[PullRequestNode]: From 78750284c5a91ef5fca02b8bc747d23751297a6a Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Wed, 15 Oct 2025 03:49:09 +0530 Subject: [PATCH 23/36] remove unused try/catch block --- .../[moduleKey]/issues/[issueId]/page.tsx | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index a31f765564..4ffaaba04f 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -232,18 +232,14 @@ const ModuleIssueDetailsPage = () => { disabled={!issueId || unassigning} onClick={async () => { if (!issueId || unassigning) return - try { - await unassignIssue({ - variables: { - programKey, - moduleKey, - issueNumber: Number(issueId), - userLogin: a.login, - }, - }) - } catch (error) { - throw new Error('Failed to unassign user', error as Error) - } + await unassignIssue({ + variables: { + programKey, + moduleKey, + issueNumber: Number(issueId), + userLogin: a.login, + }, + }) }} className={getButtonClassName(!issueId || unassigning)} title={unassigning ? 'Unassigning…' : `Unassign @${a.login}`} @@ -351,18 +347,14 @@ const ModuleIssueDetailsPage = () => { disabled={!issueId || assigning} onClick={async () => { if (!issueId || assigning) return - try { - await assignIssue({ - variables: { - programKey, - moduleKey, - issueNumber: Number(issueId), - userLogin: u.login, - }, - }) - } catch (error) { - throw new Error('Failed to assign user', error as Error) - } + await assignIssue({ + variables: { + programKey, + moduleKey, + issueNumber: Number(issueId), + userLogin: u.login, + }, + }) }} className={`${getButtonClassName(!issueId || assigning)} px-3 py-1`} title={ From 86be65d18058eaad1f04f878293f9d4bd409ee2a Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Thu, 16 Oct 2025 18:43:45 +0530 Subject: [PATCH 24/36] add set deadline mutation and component --- .../api/internal/mutations/module.py | 63 +++++++ backend/manage.py | 5 +- .../[moduleKey]/issues/[issueId]/page.tsx | 160 +++++++++++++++--- frontend/src/server/queries/issueQueries.ts | 18 ++ 4 files changed, 219 insertions(+), 27 deletions(-) diff --git a/backend/apps/mentorship/api/internal/mutations/module.py b/backend/apps/mentorship/api/internal/mutations/module.py index 958203b377..1715ce1580 100644 --- a/backend/apps/mentorship/api/internal/mutations/module.py +++ b/backend/apps/mentorship/api/internal/mutations/module.py @@ -14,6 +14,8 @@ UpdateModuleInput, ) from apps.mentorship.models import Mentor, Module, Program +import datetime as dt +from apps.mentorship.models.task import Task from apps.mentorship.models.issue_user_interest import IssueUserInterest from apps.nest.api.internal.permissions import IsAuthenticated from apps.owasp.models import Project @@ -198,6 +200,67 @@ def unassign_issue_from_user( return module + @strawberry.mutation(permission_classes=[IsAuthenticated]) + @transaction.atomic + def set_task_deadline( + self, + info: strawberry.Info, + *, + module_key: str, + program_key: str, + issue_number: int, + deadline_at: dt.datetime, + ) -> ModuleNode: + user = info.context.request.user + + module = ( + Module.objects.select_related("program") + .filter(key=module_key, program__key=program_key) + .first() + ) + if module is None: + raise ObjectDoesNotExist(msg="Module not found.") + + mentor = Mentor.objects.filter(nest_user=user).first() + if mentor is None: + raise PermissionDenied(msg="Only mentors can set deadlines.") + if not module.program.admins.filter(id=mentor.id).exists(): + raise PermissionDenied + + issue = ( + module.issues.select_related("repository").prefetch_related("assignees") + .filter(number=issue_number) + .first() + ) + if issue is None: + raise ObjectDoesNotExist(msg="Issue not found in this module.") + + assignees = issue.assignees.all() + if not assignees.exists(): + raise ValidationError(message="Cannot set deadline: issue has no assignees.") + + normalized_deadline = deadline_at + if timezone.is_naive(normalized_deadline): + normalized_deadline = timezone.make_aware(normalized_deadline) + + if normalized_deadline.date() < timezone.now().date(): + raise ValidationError(message="Deadline cannot be in the past.") + + now = timezone.now() + tasks_to_update: list[Task] = [] + for assignee in assignees: + task, _created = Task.objects.get_or_create( + module=module, issue=issue, assignee=assignee + ) + if task.assigned_at is None: + task.assigned_at = now + task.deadline_at = normalized_deadline + tasks_to_update.append(task) + + Task.objects.bulk_update(tasks_to_update, ["assigned_at", "deadline_at"]) + + return module + @strawberry.mutation(permission_classes=[IsAuthenticated]) @transaction.atomic def update_module(self, info: strawberry.Info, input_data: UpdateModuleInput) -> ModuleNode: diff --git a/backend/manage.py b/backend/manage.py index 56bdab97cc..9b1f3a1d41 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -5,13 +5,10 @@ import os import sys -from dotenv import load_dotenv - if __name__ == "__main__": - load_dotenv() os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.local") os.environ.setdefault("DJANGO_CONFIGURATION", "Local") from configurations.management import execute_from_command_line - execute_from_command_line(sys.argv) + execute_from_command_line(sys.argv) \ No newline at end of file diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 4ffaaba04f..bab036f954 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -14,11 +14,13 @@ import { addToast } from '@heroui/toast' import Image from 'next/image' import Link from 'next/link' import { useParams } from 'next/navigation' +import { useState } from 'react' import { ErrorDisplay } from 'app/global-error' import { ASSIGN_ISSUE_TO_USER, GET_MODULE_ISSUE_VIEW, UNASSIGN_ISSUE_FROM_USER, + SET_TASK_DEADLINE, } from 'server/queries/issueQueries' import ActionButton from 'components/ActionButton' import AnchorTitle from 'components/AnchorTitle' @@ -28,13 +30,8 @@ import SecondaryCard from 'components/SecondaryCard' import { TruncatedText } from 'components/TruncatedText' const ModuleIssueDetailsPage = () => { - const { issueId } = useParams() as { issueId: string } - - const { programKey, moduleKey } = useParams() as { - programKey: string - moduleKey: string - issueId: string - } + const params = useParams() as { programKey: string; moduleKey: string; issueId: string } + const { programKey, moduleKey, issueId } = params const { data, loading, error } = useQuery(GET_MODULE_ISSUE_VIEW, { variables: { programKey, moduleKey, number: Number(issueId) }, skip: !issueId, @@ -98,6 +95,50 @@ const ModuleIssueDetailsPage = () => { }) const issue = data?.getModule?.issueByNumber + const taskDeadline = data?.getModule?.taskDeadline as string | undefined + const [isEditingDeadline, setIsEditingDeadline] = useState(false) + const [deadlineInput, setDeadlineInput] = useState( + taskDeadline ? new Date(taskDeadline).toISOString().slice(0, 10) : '' + ) + + const getButtonClassName = (disabled: boolean) => + `inline-flex items-center justify-center rounded-md border p-1.5 text-sm ${ + disabled + ? 'cursor-not-allowed border-gray-300 text-gray-400 dark:border-gray-600' + : 'border-gray-300 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800' + }` + + const labelButtonClassName = + 'rounded-lg border border-gray-400 px-3 py-1 text-sm hover:bg-gray-200 dark:border-gray-300 dark:hover:bg-gray-700' + + const [setTaskDeadlineMutation, { loading: settingDeadline }] = useMutation(SET_TASK_DEADLINE, { + refetchQueries: [ + { + query: GET_MODULE_ISSUE_VIEW, + variables: { programKey, moduleKey, number: Number(issueId) }, + }, + ], + awaitRefetchQueries: true, + onCompleted: () => { + addToast({ + title: 'Deadline updated', + variant: 'solid', + color: 'success', + timeout: 2500, + shouldShowTimeoutProgress: true, + }) + setIsEditingDeadline(false) + }, + onError: (err) => { + addToast({ + title: 'Failed to update deadline: ' + err.message, + variant: 'solid', + color: 'danger', + timeout: 3500, + shouldShowTimeoutProgress: true, + }) + }, + }) if (error) { return @@ -110,16 +151,7 @@ const ModuleIssueDetailsPage = () => { const labels = issue.labels || [] const visibleLabels = labels.slice(0, 5) const remainingLabels = labels.length - visibleLabels.length - - const getButtonClassName = (disabled: boolean) => - `inline-flex items-center justify-center rounded-md border p-1.5 text-sm ${ - disabled - ? 'cursor-not-allowed border-gray-300 text-gray-400 dark:border-gray-600' - : 'border-gray-300 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800' - }` - - const labelButtonClassName = - 'rounded-lg border border-gray-400 px-3 py-1 text-sm hover:bg-gray-200 dark:border-gray-300 dark:hover:bg-gray-700' + const canEditDeadline = assignees.length > 0 return (
@@ -156,18 +188,100 @@ const ModuleIssueDetailsPage = () => { }> -
+
Assigned:{' '} {data?.getModule?.taskAssignedAt ? new Date(data.getModule.taskAssignedAt).toLocaleDateString() : 'Not assigned'}
-
- Deadline:{' '} - {data?.getModule?.taskDeadline - ? new Date(data.getModule.taskDeadline).toLocaleDateString() - : 'No deadline set'} + +
+
+ Deadline: + {isEditingDeadline && canEditDeadline ? ( + + setDeadlineInput(e.target.value)} + min={new Date().toISOString().slice(0, 10)} + className="h-8 rounded border border-gray-300 px-2 text-xs dark:border-gray-600" + /> + + + + ) : ( + + {(() => { + if (!taskDeadline) { + return ( + + No deadline set + + ) + } + const d = new Date(taskDeadline) + const today = new Date() + const isPast = d.setHours(0, 0, 0, 0) < today.setHours(0, 0, 0, 0) + const diffDays = Math.ceil( + (d.getTime() - today.getTime()) / (1000 * 60 * 60 * 24) + ) + const textClass = 'text-red-700 dark:text-red-300' + return ( + + {d.toLocaleDateString()}{' '} + {isPast + ? '(overdue)' + : diffDays > 0 + ? `(in ${diffDays} days)` + : '(today)'} + + ) + })()} + {canEditDeadline && ( + + )} + + )} +
diff --git a/frontend/src/server/queries/issueQueries.ts b/frontend/src/server/queries/issueQueries.ts index 9038a40649..00b0f2d13e 100644 --- a/frontend/src/server/queries/issueQueries.ts +++ b/frontend/src/server/queries/issueQueries.ts @@ -82,3 +82,21 @@ export const UNASSIGN_ISSUE_FROM_USER = gql` } } ` + +export const SET_TASK_DEADLINE = gql` + mutation SetTaskDeadline( + $programKey: String! + $moduleKey: String! + $issueNumber: Int! + $deadlineAt: DateTime! + ) { + setTaskDeadline( + programKey: $programKey + moduleKey: $moduleKey + issueNumber: $issueNumber + deadlineAt: $deadlineAt + ) { + id + } + } +` From 42e41dd84b3312406bece29cf7538956ade384eb Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Thu, 16 Oct 2025 19:07:01 +0530 Subject: [PATCH 25/36] update suggestion --- .../api/internal/mutations/module.py | 8 +++-- backend/manage.py | 2 +- .../[moduleKey]/issues/[issueId]/page.tsx | 30 ++++++++++++------- .../modules/[moduleKey]/issues/page.tsx | 6 ++-- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/backend/apps/mentorship/api/internal/mutations/module.py b/backend/apps/mentorship/api/internal/mutations/module.py index 1715ce1580..d840f639fd 100644 --- a/backend/apps/mentorship/api/internal/mutations/module.py +++ b/backend/apps/mentorship/api/internal/mutations/module.py @@ -1,5 +1,6 @@ """GraphQL mutations for mentorship modules in the mentorship app.""" +import datetime as dt import logging import strawberry @@ -14,9 +15,8 @@ UpdateModuleInput, ) from apps.mentorship.models import Mentor, Module, Program -import datetime as dt -from apps.mentorship.models.task import Task from apps.mentorship.models.issue_user_interest import IssueUserInterest +from apps.mentorship.models.task import Task from apps.nest.api.internal.permissions import IsAuthenticated from apps.owasp.models import Project @@ -211,6 +211,7 @@ def set_task_deadline( issue_number: int, deadline_at: dt.datetime, ) -> ModuleNode: + """Set a deadline for a task. User must be a mentor and an admin of the program.""" user = info.context.request.user module = ( @@ -228,7 +229,8 @@ def set_task_deadline( raise PermissionDenied issue = ( - module.issues.select_related("repository").prefetch_related("assignees") + module.issues.select_related("repository") + .prefetch_related("assignees") .filter(number=issue_number) .first() ) diff --git a/backend/manage.py b/backend/manage.py index 9b1f3a1d41..83d4673775 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -11,4 +11,4 @@ from configurations.management import execute_from_command_line - execute_from_command_line(sys.argv) \ No newline at end of file + execute_from_command_line(sys.argv) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index bab036f954..c4e6fb3a2f 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -166,10 +166,8 @@ const ModuleIssueDetailsPage = () => { {issue.organizationName}/{issue.repositoryName} • #{issue.number} {issue.state === 'open' ? 'Open' : 'Closed'} @@ -213,7 +211,8 @@ const ModuleIssueDetailsPage = () => { disabled={!deadlineInput || settingDeadline} onClick={async () => { if (!deadlineInput || settingDeadline || !issueId) return - const iso = new Date(deadlineInput + 'T23:59:59').toISOString() + const localDate = new Date(deadlineInput + 'T23:59:59') + const iso = localDate.toISOString() await setTaskDeadlineMutation({ variables: { programKey, @@ -297,12 +296,12 @@ const ModuleIssueDetailsPage = () => {
{visibleLabels.map((label, index) => ( - + ))} {remainingLabels > 0 && ( - + +{remainingLabels} more )}
@@ -406,15 +405,24 @@ const ModuleIssueDetailsPage = () => {
{pr.state === 'closed' && pr.mergedAt ? ( - + Merged ) : pr.state === 'closed' ? ( - + Closed ) : ( - + Open )} diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index e43fd96d58..016ff04200 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -190,10 +190,10 @@ const IssuesPage = () => { {issue.state === 'open' ? 'Open' : 'Closed'} From 77fa3bbc96914f46c5735b503c7b29bc0442c95a Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Thu, 16 Oct 2025 19:17:55 +0530 Subject: [PATCH 26/36] update fields --- .../modules/[moduleKey]/issues/page.tsx | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 016ff04200..97517342db 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -8,7 +8,6 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation' import { useEffect, useMemo, useState } from 'react' import { ErrorDisplay, handleAppError } from 'app/global-error' import { GET_MODULE_ISSUES } from 'server/queries/moduleQueries' -import type { Issue } from 'types/issue' import LoadingSpinner from 'components/LoadingSpinner' import Pagination from 'components/Pagination' @@ -40,24 +39,21 @@ const IssuesPage = () => { }, [error]) const moduleData = data?.getModule - const moduleIssues: Issue[] = useMemo(() => { - return (moduleData?.issues || []).map((i) => ({ - author: i.author, - createdAt: i.createdAt, - hint: '', - labels: i.labels || [], + type ModuleIssueRow = { + objectID: string + number: number + title: string + state: string + labels: string[] + } + + const moduleIssues: ModuleIssueRow[] = useMemo(() => { + return (moduleData?.issues || []).map((i: any) => ({ + objectID: i.id, number: i.number, - organizationName: i.organizationName, - projectName: moduleData?.projectName || '', - projectUrl: '', - repository: undefined, - repositoryLanguages: [], - body: i.body || '', title: i.title, state: i.state, - updatedAt: i.createdAt, - url: i.url, - objectID: i.id, + labels: i.labels || [], })) }, [moduleData]) From 94af2ca38e281db0e339607a85950c62ba222374 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Thu, 16 Oct 2025 19:21:53 +0530 Subject: [PATCH 27/36] fix type --- .../programs/[programKey]/modules/[moduleKey]/issues/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 97517342db..c7c33ed458 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -48,7 +48,7 @@ const IssuesPage = () => { } const moduleIssues: ModuleIssueRow[] = useMemo(() => { - return (moduleData?.issues || []).map((i: any) => ({ + return (moduleData?.issues || []).map((i) => ({ objectID: i.id, number: i.number, title: i.title, From fd72300aa185dc9658f3e58f16e46f0fbed2e802 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Thu, 16 Oct 2025 19:27:17 +0530 Subject: [PATCH 28/36] fix type --- .../modules/[moduleKey]/issues/page.tsx | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index c7c33ed458..5b8a6539db 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -45,6 +45,7 @@ const IssuesPage = () => { title: string state: string labels: string[] + assignees: Array<{ avatarUrl: string; login: string; name: string }> } const moduleIssues: ModuleIssueRow[] = useMemo(() => { @@ -54,6 +55,7 @@ const IssuesPage = () => { title: i.title, state: i.state, labels: i.labels || [], + assignees: i.assignees || [], })) }, [moduleData]) @@ -222,29 +224,25 @@ const IssuesPage = () => {
- {((assignees) => - assignees?.length ? ( + {issue.assignees?.length ? ( +
-
- {assignees[0].login} - {assignees[0].login || assignees[0].name} -
- {assignees.length > 1 && ( -
- +{assignees.length - 1} -
- )} + {issue.assignees[0].login} + {issue.assignees[0].login || issue.assignees[0].name}
- ) : null)( - (data?.getModule?.issues || []).find((i) => i.id === issue.objectID) - ?.assignees - )} + {issue.assignees.length > 1 && ( +
+ +{issue.assignees.length - 1} +
+ )} +
+ ) : null} ))} From b322dc24d1104bfd3824db8eab79b9837a6b8a26 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Sat, 18 Oct 2025 12:30:33 +0530 Subject: [PATCH 29/36] added mobile view and resolves comments --- .../apps/github/api/internal/nodes/issue.py | 6 + .../[moduleKey]/issues/[issueId]/page.tsx | 138 ++++++++---------- .../modules/[moduleKey]/issues/page.tsx | 106 ++++++++++++-- frontend/src/server/queries/issueQueries.ts | 1 + frontend/src/server/queries/moduleQueries.ts | 1 + 5 files changed, 166 insertions(+), 86 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 1e3d29dfd9..74374d11c1 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -51,6 +51,11 @@ def labels(self) -> list[str]: """Resolve label names for the issue.""" return list(self.labels.values_list("name", flat=True)) + @strawberry.field + def is_merged(self) -> bool: + """Return True if this issue has at least one merged pull request.""" + return self.pull_requests.filter(state="closed", merged_at__isnull=False).exists() + @strawberry.field def interested_users(self) -> list[UserNode]: """Return all users who have expressed interest in this issue.""" @@ -65,3 +70,4 @@ def interested_users(self) -> list[UserNode]: def pull_requests(self) -> list[PullRequestNode]: """Return all pull requests linked to this issue.""" return list(self.pull_requests.select_related("author", "repository").all()) + diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index c4e6fb3a2f..f4e27c4edd 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -32,6 +32,26 @@ import { TruncatedText } from 'components/TruncatedText' const ModuleIssueDetailsPage = () => { const params = useParams() as { programKey: string; moduleKey: string; issueId: string } const { programKey, moduleKey, issueId } = params + + const formatDeadline = (deadline: string | null) => { + if (!deadline) return { text: 'No deadline set', color: 'text-gray-600 dark:text-gray-300' } + + const deadlineDate = new Date(deadline) + const today = new Date() + const isOverdue = deadlineDate < today + const daysLeft = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)) + + const statusText = isOverdue + ? '(overdue)' + : daysLeft === 0 + ? '(today)' + : `(${daysLeft} days left)` + + return { + text: `${deadlineDate.toLocaleDateString()} ${statusText}`, + color: 'text-[#DA3633]', + } + } const { data, loading, error } = useQuery(GET_MODULE_ISSUE_VIEW, { variables: { programKey, moduleKey, number: Number(issueId) }, skip: !issueId, @@ -167,10 +187,14 @@ const ModuleIssueDetailsPage = () => { - {issue.state === 'open' ? 'Open' : 'Closed'} + {issue.state === 'open' ? 'Open' : issue.isMerged ? 'Merged' : 'Closed'}
@@ -195,23 +219,18 @@ const ModuleIssueDetailsPage = () => {
-
- Deadline: +
+ Deadline: {isEditingDeadline && canEditDeadline ? ( - - setDeadlineInput(e.target.value)} - min={new Date().toISOString().slice(0, 10)} - className="h-8 rounded border border-gray-300 px-2 text-xs dark:border-gray-600" - /> - - - + } + }} + min={new Date().toISOString().slice(0, 10)} + className="h-8 rounded border border-gray-300 px-2 text-xs dark:border-gray-600" + /> ) : ( - - {(() => { - if (!taskDeadline) { - return ( - - No deadline set - + - )} - + )}
@@ -407,21 +397,21 @@ const ModuleIssueDetailsPage = () => { {pr.state === 'closed' && pr.mergedAt ? ( Merged ) : pr.state === 'closed' ? ( Closed ) : ( Open diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 5b8a6539db..85b54b7d52 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -44,6 +44,7 @@ const IssuesPage = () => { number: number title: string state: string + isMerged: boolean labels: string[] assignees: Array<{ avatarUrl: string; login: string; name: string }> } @@ -54,6 +55,7 @@ const IssuesPage = () => { number: i.number, title: i.title, state: i.state, + isMerged: i.isMerged, labels: i.labels || [], assignees: i.assignees || [], })) @@ -135,7 +137,8 @@ const IssuesPage = () => {
-
+ {/* Desktop Table - unchanged */} +
@@ -186,16 +189,20 @@ const IssuesPage = () => { -
- - {issue.state === 'open' ? 'Open' : 'Closed'} - + +
+ + {issue.state === 'open' ? 'Open' : issue.isMerged ? 'Merged' : 'Closed'} + +
@@ -234,7 +241,9 @@ const IssuesPage = () => { alt={issue.assignees[0].login} className="rounded-full" /> - {issue.assignees[0].login || issue.assignees[0].name} + + {issue.assignees[0].login || issue.assignees[0].name} +
{issue.assignees.length > 1 && (
@@ -260,6 +269,79 @@ const IssuesPage = () => {
+ {/* Mobile & Tablet Cards */} +
+ {moduleIssues.map((issue) => ( +
+
+ + + {issue.state === 'open' ? 'Open' : issue.isMerged ? 'Merged' : 'Closed'} + +
+ + {issue.labels?.length > 0 && ( +
+ {issue.labels.slice(0, 3).map((label) => ( + + {label} + + ))} + {issue.labels.length > 3 && ( + + +{issue.labels.length - 3} + + )} +
+ )} + + {issue.assignees?.length > 0 && ( +
+ {issue.assignees[0].login} + + {issue.assignees[0].login || issue.assignees[0].name} + {issue.assignees.length > 1 && ` +${issue.assignees.length - 1}`} + +
+ )} +
+ ))} + + {moduleIssues.length === 0 && ( +
+

+ No issues found for the selected filter. +

+
+ )} +
+ {/* Pagination Controls */} Date: Sat, 18 Oct 2025 12:43:27 +0530 Subject: [PATCH 30/36] fix test cases --- backend/apps/github/api/internal/nodes/issue.py | 1 - backend/tests/apps/github/api/internal/nodes/issue_test.py | 1 + .../programs/[programKey]/modules/[moduleKey]/issues/page.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 74374d11c1..b0fa914dde 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -70,4 +70,3 @@ def interested_users(self) -> list[UserNode]: def pull_requests(self) -> list[PullRequestNode]: """Return all pull requests linked to this issue.""" return list(self.pull_requests.select_related("author", "repository").all()) - diff --git a/backend/tests/apps/github/api/internal/nodes/issue_test.py b/backend/tests/apps/github/api/internal/nodes/issue_test.py index b0ed33467e..14e7261d19 100644 --- a/backend/tests/apps/github/api/internal/nodes/issue_test.py +++ b/backend/tests/apps/github/api/internal/nodes/issue_test.py @@ -26,6 +26,7 @@ def test_issue_node_fields(self): "repository_name", "assignees", "labels", + "is_merged", "interested_users", "pull_requests", } diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 85b54b7d52..ae82173356 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -274,7 +274,7 @@ const IssuesPage = () => { {moduleIssues.map((issue) => (
+
) : ( )} diff --git a/frontend/src/server/queries/issueQueries.ts b/frontend/src/server/queries/issueQueries.ts index db5f54fde8..b5f283c97c 100644 --- a/frontend/src/server/queries/issueQueries.ts +++ b/frontend/src/server/queries/issueQueries.ts @@ -101,3 +101,11 @@ export const SET_TASK_DEADLINE = gql` } } ` + +export const CLEAR_TASK_DEADLINE = gql` + mutation ClearTaskDeadline($programKey: String!, $moduleKey: String!, $issueNumber: Int!) { + clearTaskDeadline(programKey: $programKey, moduleKey: $moduleKey, issueNumber: $issueNumber) { + id + } + } +` From d327b38fa0ceac1d9ba7cf6baa1cf897ccacc7e2 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Sun, 19 Oct 2025 11:49:03 +0530 Subject: [PATCH 33/36] logic seperation --- .../[moduleKey]/issues/[issueId]/page.tsx | 146 ++---------------- frontend/src/hooks/useIssueMutations.ts | 144 +++++++++++++++++ 2 files changed, 161 insertions(+), 129 deletions(-) create mode 100644 frontend/src/hooks/useIssueMutations.ts diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 61a8e7f8d9..5ea6b998df 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useMutation, useQuery } from '@apollo/client' +import { useQuery } from '@apollo/client' import { faCodeBranch, faLink, @@ -10,19 +10,12 @@ import { faXmark, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { addToast } from '@heroui/toast' +import { useIssueMutations } from 'hooks/useIssueMutations' import Image from 'next/image' import Link from 'next/link' import { useParams } from 'next/navigation' -import { useState } from 'react' import { ErrorDisplay } from 'app/global-error' -import { - ASSIGN_ISSUE_TO_USER, - CLEAR_TASK_DEADLINE, - GET_MODULE_ISSUE_VIEW, - UNASSIGN_ISSUE_FROM_USER, - SET_TASK_DEADLINE, -} from 'server/queries/issueQueries' +import { GET_MODULE_ISSUE_VIEW } from 'server/queries/issueQueries' import ActionButton from 'components/ActionButton' import AnchorTitle from 'components/AnchorTitle' import LoadingSpinner from 'components/LoadingSpinner' @@ -74,128 +67,23 @@ const ModuleIssueDetailsPage = () => { nextFetchPolicy: 'cache-and-network', }) - const [assignIssue, { loading: assigning }] = useMutation(ASSIGN_ISSUE_TO_USER, { - refetchQueries: [ - { - query: GET_MODULE_ISSUE_VIEW, - variables: { programKey, moduleKey, number: Number(issueId) }, - }, - ], - awaitRefetchQueries: true, - onCompleted: () => { - addToast({ - title: 'Issue assigned successfully', - variant: 'solid', - color: 'success', - timeout: 3000, - shouldShowTimeoutProgress: true, - }) - }, - onError: (error) => { - addToast({ - title: 'Failed to assign issue: ' + error.message, - variant: 'solid', - color: 'danger', - timeout: 3000, - shouldShowTimeoutProgress: true, - }) - }, - }) - const [unassignIssue, { loading: unassigning }] = useMutation(UNASSIGN_ISSUE_FROM_USER, { - refetchQueries: [ - { - query: GET_MODULE_ISSUE_VIEW, - variables: { programKey, moduleKey, number: Number(issueId) }, - }, - ], - awaitRefetchQueries: true, - onCompleted: () => { - addToast({ - title: 'Issue unassigned successfully', - variant: 'solid', - color: 'success', - timeout: 3000, - shouldShowTimeoutProgress: true, - }) - }, - onError: (error) => { - addToast({ - title: 'Failed to unassign issue: ' + error.message, - variant: 'solid', - color: 'danger', - timeout: 3000, - shouldShowTimeoutProgress: true, - }) - }, - }) - - const [setTaskDeadlineMutation, { loading: settingDeadline }] = useMutation(SET_TASK_DEADLINE, { - refetchQueries: [ - { - query: GET_MODULE_ISSUE_VIEW, - variables: { programKey, moduleKey, number: Number(issueId) }, - }, - ], - awaitRefetchQueries: true, - onCompleted: () => { - addToast({ - title: 'Deadline updated', - variant: 'solid', - color: 'success', - timeout: 2500, - shouldShowTimeoutProgress: true, - }) - setIsEditingDeadline(false) - }, - onError: (err) => { - addToast({ - title: 'Failed to update deadline: ' + err.message, - variant: 'solid', - color: 'danger', - timeout: 3500, - shouldShowTimeoutProgress: true, - }) - }, - }) - - const [clearTaskDeadlineMutation, { loading: clearingDeadline }] = useMutation( - CLEAR_TASK_DEADLINE, - { - refetchQueries: [ - { - query: GET_MODULE_ISSUE_VIEW, - variables: { programKey, moduleKey, number: Number(issueId) }, - }, - ], - awaitRefetchQueries: true, - onCompleted: () => { - addToast({ - title: 'Deadline cleared', - variant: 'solid', - color: 'success', - timeout: 2500, - shouldShowTimeoutProgress: true, - }) - setIsEditingDeadline(false) - }, - onError: (err) => { - addToast({ - title: 'Failed to clear deadline: ' + err.message, - variant: 'solid', - color: 'danger', - timeout: 3500, - shouldShowTimeoutProgress: true, - }) - }, - } - ) + const { + assignIssue, + unassignIssue, + setTaskDeadlineMutation, + clearTaskDeadlineMutation, + assigning, + unassigning, + settingDeadline, + clearingDeadline, + isEditingDeadline, + setIsEditingDeadline, + deadlineInput, + setDeadlineInput, + } = useIssueMutations({ programKey, moduleKey, issueId }) const issue = data?.getModule?.issueByNumber const taskDeadline = data?.getModule?.taskDeadline as string | undefined - const [isEditingDeadline, setIsEditingDeadline] = useState(false) - const [deadlineInput, setDeadlineInput] = useState( - taskDeadline ? new Date(taskDeadline).toISOString().slice(0, 10) : '' - ) const getButtonClassName = (disabled: boolean) => `inline-flex items-center justify-center rounded-md border p-1.5 text-sm ${ diff --git a/frontend/src/hooks/useIssueMutations.ts b/frontend/src/hooks/useIssueMutations.ts new file mode 100644 index 0000000000..307506c697 --- /dev/null +++ b/frontend/src/hooks/useIssueMutations.ts @@ -0,0 +1,144 @@ +import { useMutation } from '@apollo/client' +import { addToast } from '@heroui/toast' +import { useState } from 'react' +import { + ASSIGN_ISSUE_TO_USER, + CLEAR_TASK_DEADLINE, + GET_MODULE_ISSUE_VIEW, + SET_TASK_DEADLINE, + UNASSIGN_ISSUE_FROM_USER, +} from 'server/queries/issueQueries' + +interface UseIssueMutationsProps { + programKey: string + moduleKey: string + issueId: string +} + +export const useIssueMutations = ({ programKey, moduleKey, issueId }: UseIssueMutationsProps) => { + const [isEditingDeadline, setIsEditingDeadline] = useState(false) + const [deadlineInput, setDeadlineInput] = useState('') + + const commonMutationConfig = { + refetchQueries: [ + { + query: GET_MODULE_ISSUE_VIEW, + variables: { programKey, moduleKey, number: Number(issueId) }, + }, + ], + awaitRefetchQueries: true, + } + + const [assignIssue, { loading: assigning }] = useMutation(ASSIGN_ISSUE_TO_USER, { + ...commonMutationConfig, + onCompleted: () => { + addToast({ + title: 'Issue assigned successfully', + variant: 'solid', + color: 'success', + timeout: 3000, + shouldShowTimeoutProgress: true, + }) + }, + onError: (error) => { + addToast({ + title: 'Failed to assign issue: ' + error.message, + variant: 'solid', + color: 'danger', + timeout: 3000, + shouldShowTimeoutProgress: true, + }) + }, + }) + + const [unassignIssue, { loading: unassigning }] = useMutation(UNASSIGN_ISSUE_FROM_USER, { + ...commonMutationConfig, + onCompleted: () => { + addToast({ + title: 'Issue unassigned successfully', + variant: 'solid', + color: 'success', + timeout: 3000, + shouldShowTimeoutProgress: true, + }) + }, + onError: (error) => { + addToast({ + title: 'Failed to unassign issue: ' + error.message, + variant: 'solid', + color: 'danger', + timeout: 3000, + shouldShowTimeoutProgress: true, + }) + }, + }) + + const [setTaskDeadlineMutation, { loading: settingDeadline }] = useMutation(SET_TASK_DEADLINE, { + ...commonMutationConfig, + onCompleted: () => { + addToast({ + title: 'Deadline updated', + variant: 'solid', + color: 'success', + timeout: 2500, + shouldShowTimeoutProgress: true, + }) + setIsEditingDeadline(false) + }, + onError: (err) => { + addToast({ + title: 'Failed to update deadline: ' + err.message, + variant: 'solid', + color: 'danger', + timeout: 3500, + shouldShowTimeoutProgress: true, + }) + }, + }) + + const [clearTaskDeadlineMutation, { loading: clearingDeadline }] = useMutation( + CLEAR_TASK_DEADLINE, + { + ...commonMutationConfig, + onCompleted: () => { + addToast({ + title: 'Deadline cleared', + variant: 'solid', + color: 'success', + timeout: 2500, + shouldShowTimeoutProgress: true, + }) + setIsEditingDeadline(false) + }, + onError: (err) => { + addToast({ + title: 'Failed to clear deadline: ' + err.message, + variant: 'solid', + color: 'danger', + timeout: 3500, + shouldShowTimeoutProgress: true, + }) + }, + } + ) + + return { + // Mutations + assignIssue, + unassignIssue, + setTaskDeadlineMutation, + clearTaskDeadlineMutation, + + // Loading states + assigning, + unassigning, + settingDeadline, + clearingDeadline, + + // Deadline state + isEditingDeadline, + setIsEditingDeadline, + deadlineInput, + setDeadlineInput, + } +} From 7165506cc08a22b81359c77847e98c436f927c39 Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Tue, 21 Oct 2025 21:02:43 +0530 Subject: [PATCH 34/36] trigger on close button --- .../[moduleKey]/issues/[issueId]/page.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 5ea6b998df..56c0544cf7 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -198,25 +198,6 @@ const ModuleIssueDetailsPage = () => { min={new Date().toISOString().slice(0, 10)} className="h-8 rounded border border-gray-300 px-2 dark:border-gray-600" /> -
) : (
+ } + > +
+ {displayContributors.map((item, index) => ( +
+
+ {item?.name + + {upperFirst(item.name) || upperFirst(item.login)} + +
+
+ ))} +
+ {contributors.length > maxInitialDisplay && } + + ) +} + +export default MenteeContributorsList diff --git a/frontend/src/components/MenteeIssues.tsx b/frontend/src/components/MenteeIssues.tsx new file mode 100644 index 0000000000..721256ece2 --- /dev/null +++ b/frontend/src/components/MenteeIssues.tsx @@ -0,0 +1,151 @@ +import { faBug, faCheckCircle, faClock } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import type React from 'react' +import { useState } from 'react' +import type { Issue } from 'types/issue' +import { formatDate } from 'utils/dateFormatter' +import SecondaryCard from 'components/SecondaryCard' + +interface MenteeIssuesProps { + openIssues: Issue[] + closedIssues: Issue[] + menteeHandle: string +} + +const MenteeIssues: React.FC = ({ openIssues, closedIssues, menteeHandle }) => { + const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open') + + const getStateColor = (state: string) => { + switch (state.toLowerCase()) { + case 'open': + return 'text-green-600 bg-green-100' + case 'closed': + return 'text-red-600 bg-red-100' + case 'merged': + return 'text-purple-600 bg-purple-100' + default: + return 'text-gray-600 bg-gray-100' + } + } + + const getStateIcon = (state: string) => { + switch (state.toLowerCase()) { + case 'open': + return faBug + case 'closed': + return faCheckCircle + default: + return faClock + } + } + + const renderIssueList = (issues: Issue[], title: string) => ( +
+ {issues.length === 0 ? ( +
+ +

No {title.toLowerCase()} issues

+
+ ) : ( + issues.map((issue) => ( +
+
+
+
+ +

{issue.title}

+ + {issue.state || 'open'} + +
+ + {issue.labels && issue.labels.length > 0 && ( +
+ {issue.labels.slice(0, 3).map((label, index) => ( + + {label} + + ))} + {issue.labels.length > 3 && ( + + +{issue.labels.length - 3} more + + )} +
+ )} + +
+ #{issue.number} + Created: {formatDate(issue.createdAt)} + {issue.updatedAt && Updated: {formatDate(issue.updatedAt)}} +
+
+ + +
+
+ )) + )} +
+ ) + + return ( + + {/* Tab Navigation */} +
+ + +
+ + {/* Issue Content */} + {activeTab === 'open' + ? renderIssueList(openIssues, 'Open') + : renderIssueList(closedIssues, 'Closed')} +
+ ) +} + +export default MenteeIssues diff --git a/frontend/src/server/queries/issueQueries.ts b/frontend/src/server/queries/issueQueries.ts index b5f283c97c..873c6c06e6 100644 --- a/frontend/src/server/queries/issueQueries.ts +++ b/frontend/src/server/queries/issueQueries.ts @@ -44,6 +44,12 @@ export const GET_MODULE_ISSUE_VIEW = gql` name avatarUrl } + issueMentees(issueNumber: $number) { + id + login + name + avatarUrl + } } } ` diff --git a/frontend/src/server/queries/menteeQueries.ts b/frontend/src/server/queries/menteeQueries.ts new file mode 100644 index 0000000000..e87c81db31 --- /dev/null +++ b/frontend/src/server/queries/menteeQueries.ts @@ -0,0 +1,35 @@ +import { gql } from '@apollo/client' + +export const GET_MODULE_MENTEE_DETAILS = gql` + query GetModuleMenteeDetails($programKey: String!, $moduleKey: String!, $menteeHandle: String!) { + getMenteeDetails(programKey: $programKey, moduleKey: $moduleKey, menteeHandle: $menteeHandle) { + id + login + name + avatarUrl + bio + experienceLevel + domains + tags + } + getMenteeModuleIssues( + programKey: $programKey + moduleKey: $moduleKey + menteeHandle: $menteeHandle + limit: 50 + ) { + id + number + title + state + labels + assignees { + login + name + avatarUrl + } + createdAt + url + } + } +` diff --git a/frontend/src/server/queries/moduleQueries.ts b/frontend/src/server/queries/moduleQueries.ts index 5d4e8727e2..408178bab9 100644 --- a/frontend/src/server/queries/moduleQueries.ts +++ b/frontend/src/server/queries/moduleQueries.ts @@ -17,6 +17,12 @@ export const GET_MODULES_BY_PROGRAM = gql` login avatarUrl } + mentees { + id + login + name + avatarUrl + } } } ` @@ -39,6 +45,12 @@ export const GET_MODULE_BY_ID = gql` name avatarUrl } + mentees { + id + login + name + avatarUrl + } } } ` @@ -73,6 +85,12 @@ export const GET_PROGRAM_ADMINS_AND_MODULES = gql` name avatarUrl } + mentees { + id + login + name + avatarUrl + } } } ` diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts index adb2c61067..7285f888e9 100644 --- a/frontend/src/types/__generated__/graphql.ts +++ b/frontend/src/types/__generated__/graphql.ts @@ -190,6 +190,7 @@ export type IssueNode = Node & { /** The Globally Unique ID of this object */ id: Scalars['ID']['output']; interestedUsers: Array; + isMerged: Scalars['Boolean']['output']; labels: Array; number: Scalars['Int']['output']; organizationName?: Maybe; @@ -207,6 +208,18 @@ export type LogoutResult = { ok: Scalars['Boolean']['output']; }; +export type MenteeNode = { + __typename?: 'MenteeNode'; + avatarUrl: Scalars['String']['output']; + bio?: Maybe; + domains?: Maybe>; + experienceLevel: Scalars['String']['output']; + id: Scalars['String']['output']; + login: Scalars['String']['output']; + name: Scalars['String']['output']; + tags?: Maybe>; +}; + export type MentorNode = { __typename?: 'MentorNode'; avatarUrl: Scalars['String']['output']; @@ -242,10 +255,12 @@ export type ModuleNode = { id: Scalars['ID']['output']; interestedUsers: Array; issueByNumber?: Maybe; + issueMentees: Array; issues: Array; issuesCount: Scalars['Int']['output']; key: Scalars['String']['output']; labels?: Maybe>; + mentees: Array; mentors: Array; name: Scalars['String']['output']; program?: Maybe; @@ -268,6 +283,11 @@ export type ModuleNodeIssueByNumberArgs = { }; +export type ModuleNodeIssueMenteesArgs = { + issueNumber: Scalars['Int']['input']; +}; + + export type ModuleNodeIssuesArgs = { label?: InputMaybe; limit?: Scalars['Int']['input']; @@ -292,12 +312,14 @@ export type ModuleNodeTaskDeadlineArgs = { export type Mutation = { __typename?: 'Mutation'; assignIssueToUser: ModuleNode; + clearTaskDeadline: ModuleNode; createApiKey: CreateApiKeyResult; createModule: ModuleNode; createProgram: ProgramNode; githubAuth: GitHubAuthResult; logoutUser: LogoutResult; revokeApiKey: RevokeApiKeyResult; + setTaskDeadline: ModuleNode; unassignIssueFromUser: ModuleNode; updateModule: ModuleNode; updateProgram: ProgramNode; @@ -313,6 +335,13 @@ export type MutationAssignIssueToUserArgs = { }; +export type MutationClearTaskDeadlineArgs = { + issueNumber: Scalars['Int']['input']; + moduleKey: Scalars['String']['input']; + programKey: Scalars['String']['input']; +}; + + export type MutationCreateApiKeyArgs = { expiresAt: Scalars['DateTime']['input']; name: Scalars['String']['input']; @@ -339,6 +368,14 @@ export type MutationRevokeApiKeyArgs = { }; +export type MutationSetTaskDeadlineArgs = { + deadlineAt: Scalars['DateTime']['input']; + issueNumber: Scalars['Int']['input']; + moduleKey: Scalars['String']['input']; + programKey: Scalars['String']['input']; +}; + + export type MutationUnassignIssueFromUserArgs = { issueNumber: Scalars['Int']['input']; moduleKey: Scalars['String']['input']; @@ -574,6 +611,8 @@ export type Query = { apiKeys: Array; chapter?: Maybe; committee?: Maybe; + getMenteeDetails: MenteeNode; + getMenteeModuleIssues: Array; getModule: ModuleNode; getProgram: ProgramNode; getProgramModules: Array; @@ -618,6 +657,22 @@ export type QueryCommitteeArgs = { }; +export type QueryGetMenteeDetailsArgs = { + menteeHandle: Scalars['String']['input']; + moduleKey: Scalars['String']['input']; + programKey: Scalars['String']['input']; +}; + + +export type QueryGetMenteeModuleIssuesArgs = { + limit?: Scalars['Int']['input']; + menteeHandle: Scalars['String']['input']; + moduleKey: Scalars['String']['input']; + offset?: Scalars['Int']['input']; + programKey: Scalars['String']['input']; +}; + + export type QueryGetModuleArgs = { moduleKey: Scalars['String']['input']; programKey: Scalars['String']['input']; diff --git a/frontend/src/types/__generated__/issueQueries.generated.ts b/frontend/src/types/__generated__/issueQueries.generated.ts index 927c67dda9..269f0087cf 100644 --- a/frontend/src/types/__generated__/issueQueries.generated.ts +++ b/frontend/src/types/__generated__/issueQueries.generated.ts @@ -8,7 +8,7 @@ export type GetModuleIssueViewQueryVariables = Types.Exact<{ }>; -export type GetModuleIssueViewQuery = { getModule: { __typename: 'ModuleNode', id: string, taskDeadline: unknown | null, taskAssignedAt: unknown | null, issueByNumber: { __typename: 'IssueNode', id: string, number: number, title: string, body: string, url: string, state: string, organizationName: string | null, repositoryName: string | null, labels: Array, assignees: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }>, pullRequests: Array<{ __typename: 'PullRequestNode', id: string, title: string, url: string, state: string, createdAt: unknown, mergedAt: unknown | null, author: { __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string } | null }> } | null, interestedUsers: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }> } }; +export type GetModuleIssueViewQuery = { getModule: { __typename: 'ModuleNode', id: string, taskDeadline: unknown | null, taskAssignedAt: unknown | null, issueByNumber: { __typename: 'IssueNode', id: string, number: number, title: string, body: string, url: string, state: string, isMerged: boolean, organizationName: string | null, repositoryName: string | null, labels: Array, assignees: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }>, pullRequests: Array<{ __typename: 'PullRequestNode', id: string, title: string, url: string, state: string, createdAt: unknown, mergedAt: unknown | null, author: { __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string } | null }> } | null, interestedUsers: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }>, issueMentees: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }> } }; export type AssignIssueToUserMutationVariables = Types.Exact<{ programKey: Types.Scalars['String']['input']; @@ -30,7 +30,28 @@ export type UnassignIssueFromUserMutationVariables = Types.Exact<{ export type UnassignIssueFromUserMutation = { unassignIssueFromUser: { __typename: 'ModuleNode', id: string } }; +export type SetTaskDeadlineMutationVariables = Types.Exact<{ + programKey: Types.Scalars['String']['input']; + moduleKey: Types.Scalars['String']['input']; + issueNumber: Types.Scalars['Int']['input']; + deadlineAt: Types.Scalars['DateTime']['input']; +}>; + + +export type SetTaskDeadlineMutation = { setTaskDeadline: { __typename: 'ModuleNode', id: string } }; + +export type ClearTaskDeadlineMutationVariables = Types.Exact<{ + programKey: Types.Scalars['String']['input']; + moduleKey: Types.Scalars['String']['input']; + issueNumber: Types.Scalars['Int']['input']; +}>; + + +export type ClearTaskDeadlineMutation = { clearTaskDeadline: { __typename: 'ModuleNode', id: string } }; + -export const GetModuleIssueViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleIssueView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"number"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"taskDeadline"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}]},{"kind":"Field","name":{"kind":"Name","value":"taskAssignedAt"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}]},{"kind":"Field","name":{"kind":"Name","value":"issueByNumber"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"number"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"pullRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"mergedAt"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"interestedUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetModuleIssueViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleIssueView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"number"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"taskDeadline"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}]},{"kind":"Field","name":{"kind":"Name","value":"taskAssignedAt"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}]},{"kind":"Field","name":{"kind":"Name","value":"issueByNumber"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"number"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"isMerged"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"pullRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"mergedAt"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"interestedUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"issueMentees"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"number"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; export const AssignIssueToUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AssignIssueToUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignIssueToUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}}},{"kind":"Argument","name":{"kind":"Name","value":"userLogin"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; -export const UnassignIssueFromUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnassignIssueFromUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unassignIssueFromUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}}},{"kind":"Argument","name":{"kind":"Name","value":"userLogin"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const UnassignIssueFromUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnassignIssueFromUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unassignIssueFromUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}}},{"kind":"Argument","name":{"kind":"Name","value":"userLogin"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userLogin"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; +export const SetTaskDeadlineDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetTaskDeadline"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"deadlineAt"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setTaskDeadline"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}}},{"kind":"Argument","name":{"kind":"Name","value":"deadlineAt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"deadlineAt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; +export const ClearTaskDeadlineDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ClearTaskDeadline"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"clearTaskDeadline"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"issueNumber"},"value":{"kind":"Variable","name":{"kind":"Name","value":"issueNumber"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/menteeQueries.generated.ts b/frontend/src/types/__generated__/menteeQueries.generated.ts new file mode 100644 index 0000000000..8f89004e70 --- /dev/null +++ b/frontend/src/types/__generated__/menteeQueries.generated.ts @@ -0,0 +1,14 @@ +import * as Types from './graphql'; + +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type GetModuleMenteeDetailsQueryVariables = Types.Exact<{ + programKey: Types.Scalars['String']['input']; + moduleKey: Types.Scalars['String']['input']; + menteeHandle: Types.Scalars['String']['input']; +}>; + + +export type GetModuleMenteeDetailsQuery = { getMenteeDetails: { __typename: 'MenteeNode', id: string, login: string, name: string, avatarUrl: string, bio: string | null, experienceLevel: string, domains: Array | null, tags: Array | null }, getMenteeModuleIssues: Array<{ __typename: 'IssueNode', id: string, number: number, title: string, state: string, labels: Array, createdAt: unknown, url: string, assignees: Array<{ __typename: 'UserNode', login: string, name: string, avatarUrl: string }> }> }; + + +export const GetModuleMenteeDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleMenteeDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"menteeHandle"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getMenteeDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"menteeHandle"},"value":{"kind":"Variable","name":{"kind":"Name","value":"menteeHandle"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}}]}},{"kind":"Field","name":{"kind":"Name","value":"getMenteeModuleIssues"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"menteeHandle"},"value":{"kind":"Variable","name":{"kind":"Name","value":"menteeHandle"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"50"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/moduleQueries.generated.ts b/frontend/src/types/__generated__/moduleQueries.generated.ts index ad2133551b..b19a656778 100644 --- a/frontend/src/types/__generated__/moduleQueries.generated.ts +++ b/frontend/src/types/__generated__/moduleQueries.generated.ts @@ -6,7 +6,7 @@ export type GetModulesByProgramQueryVariables = Types.Exact<{ }>; -export type GetModulesByProgramQuery = { getProgramModules: Array<{ __typename: 'ModuleNode', id: string, key: string, name: string, description: string, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, projectId: string | null, projectName: string | null, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, avatarUrl: string }> }> }; +export type GetModulesByProgramQuery = { getProgramModules: Array<{ __typename: 'ModuleNode', id: string, key: string, name: string, description: string, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, projectId: string | null, projectName: string | null, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, avatarUrl: string }>, mentees: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }> }> }; export type GetModuleByIdQueryVariables = Types.Exact<{ moduleKey: Types.Scalars['String']['input']; @@ -14,7 +14,7 @@ export type GetModuleByIdQueryVariables = Types.Exact<{ }>; -export type GetModuleByIdQuery = { getModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, tags: Array | null, domains: Array | null, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> } }; +export type GetModuleByIdQuery = { getModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, tags: Array | null, domains: Array | null, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }>, mentees: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }> } }; export type GetProgramAdminsAndModulesQueryVariables = Types.Exact<{ programKey: Types.Scalars['String']['input']; @@ -22,7 +22,7 @@ export type GetProgramAdminsAndModulesQueryVariables = Types.Exact<{ }>; -export type GetProgramAdminsAndModulesQuery = { getProgram: { __typename: 'ProgramNode', id: string, admins: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> | null }, getModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, tags: Array | null, labels: Array | null, projectId: string | null, projectName: string | null, domains: Array | null, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> } }; +export type GetProgramAdminsAndModulesQuery = { getProgram: { __typename: 'ProgramNode', id: string, admins: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }> | null }, getModule: { __typename: 'ModuleNode', id: string, key: string, name: string, description: string, tags: Array | null, labels: Array | null, projectId: string | null, projectName: string | null, domains: Array | null, experienceLevel: Types.ExperienceLevelEnum, startedAt: unknown, endedAt: unknown, mentors: Array<{ __typename: 'MentorNode', id: string, login: string, name: string, avatarUrl: string }>, mentees: Array<{ __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string }> } }; export type GetModuleIssuesQueryVariables = Types.Exact<{ programKey: Types.Scalars['String']['input']; @@ -33,10 +33,10 @@ export type GetModuleIssuesQueryVariables = Types.Exact<{ }>; -export type GetModuleIssuesQuery = { getModule: { __typename: 'ModuleNode', name: string, issuesCount: number, availableLabels: Array, issues: Array<{ __typename: 'IssueNode', id: string, number: number, title: string, state: string, labels: Array, assignees: Array<{ __typename: 'UserNode', avatarUrl: string, login: string, name: string }> }> } }; +export type GetModuleIssuesQuery = { getModule: { __typename: 'ModuleNode', name: string, issuesCount: number, availableLabels: Array, issues: Array<{ __typename: 'IssueNode', id: string, number: number, title: string, state: string, isMerged: boolean, labels: Array, assignees: Array<{ __typename: 'UserNode', avatarUrl: string, login: string, name: string }> }> } }; -export const GetModulesByProgramDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModulesByProgram"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgramModules"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetModuleByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleByID"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetProgramAdminsAndModulesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProgramAdminsAndModules"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgram"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetModuleIssuesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleIssues"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"label"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}]},{"kind":"Field","name":{"kind":"Name","value":"availableLabels"}},{"kind":"Field","name":{"kind":"Name","value":"issues"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const GetModulesByProgramDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModulesByProgram"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgramModules"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mentees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetModuleByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleByID"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mentees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetProgramAdminsAndModulesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProgramAdminsAndModules"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgram"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mentees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetModuleIssuesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleIssues"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"label"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}]},{"kind":"Field","name":{"kind":"Name","value":"availableLabels"}},{"kind":"Field","name":{"kind":"Name","value":"issues"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"isMerged"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/card.ts b/frontend/src/types/card.ts index d0fd51b200..02986741fd 100644 --- a/frontend/src/types/card.ts +++ b/frontend/src/types/card.ts @@ -55,6 +55,7 @@ export interface DetailsCardProps { setStatus?: (newStatus: string) => void canUpdateStatus?: boolean mentors?: Contributor[] + mentees?: Contributor[] admins?: Contributor[] projectName?: string programKey?: string diff --git a/frontend/src/types/mentorship.ts b/frontend/src/types/mentorship.ts index 842dca207b..593f0e90ef 100644 --- a/frontend/src/types/mentorship.ts +++ b/frontend/src/types/mentorship.ts @@ -57,6 +57,7 @@ export type Module = { status: ProgramStatusEnum experienceLevel: ExperienceLevelEnum mentors: Contributor[] + mentees?: Contributor[] startedAt: string endedAt: string domains: string[] @@ -77,3 +78,46 @@ export type ModuleFormData = { projectId: string mentorLogins: string } + +import type { Issue } from 'types/issue' + +export type CompletedLevel = { + id: string + name: string + level: number + completedAt: string + stack: string + description: string +} + +export type Achievement = { + id: string + name: string + description: string + earnedAt: string + type: string + points: number +} + +export type Penalty = { + id: string + reason: string + points: number + createdAt: string + status: string + description: string +} + +export type MenteeDetails = { + id: string + login: string + name: string + avatarUrl: string + email?: string + bio?: string + completedLevels: CompletedLevel[] + achievements: Achievement[] + penalties: Penalty[] + openIssues: Issue[] + closedIssues: Issue[] +} From 4bc7c4971e41992975f6b013d482d985d92eab9e Mon Sep 17 00:00:00 2001 From: Rajgupta36 Date: Fri, 7 Nov 2025 17:38:57 +0530 Subject: [PATCH 36/36] update sugggestoins --- .../mentees/[menteeHandle]/page.tsx | 21 ++++++++++++------- frontend/src/types/mentorship.ts | 2 ++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeHandle]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeHandle]/page.tsx index 7364b1a7c4..cd5d4fcb7e 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeHandle]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeHandle]/page.tsx @@ -6,6 +6,8 @@ import { useParams } from 'next/navigation' import { useEffect, useState } from 'react' import { ErrorDisplay, handleAppError } from 'app/global-error' import { GET_MODULE_MENTEE_DETAILS } from 'server/queries/menteeQueries' +import { Issue } from 'types/issue' +import { MenteeDetails } from 'types/mentorship' import { LabelList } from 'components/LabelList' import LoadingSpinner from 'components/LoadingSpinner' import SecondaryCard from 'components/SecondaryCard' @@ -17,8 +19,9 @@ const MenteeProfilePage = () => { menteeHandle: string } - const [menteeDetails, setMenteeDetails] = useState(null) - const [menteeIssues, setMenteeIssues] = useState([]) + const [menteeDetails, setMenteeDetails] = useState(null) + const [menteeIssues, setMenteeIssues] = useState([]) + const [isLoading, setIsLoading] = useState(true) const [statusFilter, setStatusFilter] = useState('all') @@ -33,12 +36,14 @@ const MenteeProfilePage = () => { }) useEffect(() => { - if (data?.getMenteeDetails) { - setMenteeDetails(data.getMenteeDetails) - setMenteeIssues(data.getMenteeModuleIssues || []) - setIsLoading(false) - } else if (error) { + if (data) { + setMenteeDetails(data.getMenteeDetails ?? null) + setMenteeIssues(data.getMenteeModuleIssues ?? []) + } + if (error) { handleAppError(error) + } + if (data || error) { setIsLoading(false) } }, [data, error]) @@ -169,7 +174,7 @@ const MenteeProfilePage = () => {
{filteredIssues.map((issue) => (
diff --git a/frontend/src/types/mentorship.ts b/frontend/src/types/mentorship.ts index 593f0e90ef..4449347d45 100644 --- a/frontend/src/types/mentorship.ts +++ b/frontend/src/types/mentorship.ts @@ -115,6 +115,8 @@ export type MenteeDetails = { avatarUrl: string email?: string bio?: string + domains?: string[] + tags?: string[] completedLevels: CompletedLevel[] achievements: Achievement[] penalties: Penalty[]