From 0be567e397da44107a27a25ed7e0e0b7b0af5b1c Mon Sep 17 00:00:00 2001 From: Mika Pons Date: Fri, 18 Sep 2026 12:26:48 +0200 Subject: [PATCH 01/23] =?UTF-8?q?=F0=9F=90=9B=20Fix=20tooltip=20content=20?= =?UTF-8?q?adaptation,=20helper=20expansion=20and=20viewport=20boundaries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graphs/commits/commits.component.scss | 28 +++++++++++----- src/app/services/tooltip.service.ts | 32 +++++++++++-------- .../shared/ui/tooltip/tooltip.component.html | 10 ++++-- .../shared/ui/tooltip/tooltip.component.scss | 24 +++++++++++++- 4 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/app/components/graphs/commits/commits.component.scss b/src/app/components/graphs/commits/commits.component.scss index e2c3c92d..7839e358 100644 --- a/src/app/components/graphs/commits/commits.component.scss +++ b/src/app/components/graphs/commits/commits.component.scss @@ -24,7 +24,7 @@ width: 100%; --chart-width-left-spacing-ratio: 85%; --chart-width-max-left-spacing: 150px; - // to change chart-with + /* to change chart-width */ flex: 1; } @@ -87,8 +87,9 @@ flex-direction: column; gap: 2px; user-select: none; - min-width: 200px; - max-width: 300px; + width: max-content; + min-width: max-content; + max-width: min(420px, calc(100vw - 32px)); .repo-name { font-weight: 600; @@ -118,14 +119,29 @@ } .tooltip-helpers { - gap: 0.75rem; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.5rem 0.75rem; font-size: 0.75rem; color: var(--color-text-muted); white-space: nowrap; + width: 100%; + min-width: max-content; + flex-wrap: wrap; + + > span { + white-space: nowrap; + flex-shrink: 0; + } } .tooltip-helper-item { + display: inline-flex; + align-items: center; gap: 0.25rem; + white-space: nowrap; + flex-shrink: 0; .ext-icon { width: 12px; @@ -135,7 +151,3 @@ } } -#commit_hover { - max-width: 340px; -} - diff --git a/src/app/services/tooltip.service.ts b/src/app/services/tooltip.service.ts index 61ae8415..2ab05755 100644 --- a/src/app/services/tooltip.service.ts +++ b/src/app/services/tooltip.service.ts @@ -186,28 +186,36 @@ export class TooltipService { ): { top: number; left: number } { let { t: top, l: left } = this.calculatePosition(rect, tooltipRect, placement, offset); - // Dynamic placement adjustment to prevent hiding the hovered item + // Dynamic placement adjustment to prevent hiding the hovered item or overflowing the viewport if (placement === "right" && left + tooltipRect.width > window.innerWidth - 8) { const alternative = this.calculatePosition(rect, tooltipRect, "left", offset); - if (alternative.l >= 8) { + const roomRight = window.innerWidth - 8 - (rect.right + offset); + const roomLeft = rect.left - offset - 8; + if (alternative.l >= 8 || roomLeft > roomRight) { top = alternative.t; left = alternative.l; } } else if (placement === "left" && left < 8) { const alternative = this.calculatePosition(rect, tooltipRect, "right", offset); - if (alternative.l + tooltipRect.width <= window.innerWidth - 8) { + const roomLeft = rect.left - offset - 8; + const roomRight = window.innerWidth - 8 - (rect.right + offset); + if (alternative.l + tooltipRect.width <= window.innerWidth - 8 || roomRight > roomLeft) { top = alternative.t; left = alternative.l; } } else if (placement === "top" && top < 8) { const alternative = this.calculatePosition(rect, tooltipRect, "bottom", offset); - if (alternative.t + tooltipRect.height <= window.innerHeight - 8) { + const roomTop = rect.top - offset - 8; + const roomBottom = window.innerHeight - 8 - (rect.bottom + offset); + if (alternative.t + tooltipRect.height <= window.innerHeight - 8 || roomBottom > roomTop) { top = alternative.t; left = alternative.l; } } else if (placement === "bottom" && top + tooltipRect.height > window.innerHeight - 8) { const alternative = this.calculatePosition(rect, tooltipRect, "top", offset); - if (alternative.t >= 8) { + const roomBottom = window.innerHeight - 8 - (rect.bottom + offset); + const roomTop = rect.top - offset - 8; + if (alternative.t >= 8 || roomTop > roomBottom) { top = alternative.t; left = alternative.l; } @@ -217,14 +225,12 @@ export class TooltipService { } private ensureWithinBounds(top: number, left: number, tooltipRect: DOMRect): { top: number; left: number } { - if (left < 8) left = 8; - if (top < 8) top = 8; - if (left + tooltipRect.width > window.innerWidth - 8) { - left = window.innerWidth - tooltipRect.width - 8; - } - if (top + tooltipRect.height > window.innerHeight - 8) { - top = window.innerHeight - tooltipRect.height - 8; - } + const maxLeft = Math.max(8, window.innerWidth - tooltipRect.width - 8); + const maxTop = Math.max(8, window.innerHeight - tooltipRect.height - 8); + + left = Math.min(Math.max(8, left), maxLeft); + top = Math.min(Math.max(8, top), maxTop); + return { top, left }; } diff --git a/src/app/shared/ui/tooltip/tooltip.component.html b/src/app/shared/ui/tooltip/tooltip.component.html index 33fa5a7f..2de0e2ce 100644 --- a/src/app/shared/ui/tooltip/tooltip.component.html +++ b/src/app/shared/ui/tooltip/tooltip.component.html @@ -1,5 +1,11 @@ -
- +
+ diff --git a/src/app/shared/ui/tooltip/tooltip.component.scss b/src/app/shared/ui/tooltip/tooltip.component.scss index d00a1465..dbd68a5d 100644 --- a/src/app/shared/ui/tooltip/tooltip.component.scss +++ b/src/app/shared/ui/tooltip/tooltip.component.scss @@ -6,14 +6,17 @@ position: absolute; z-index: $zindex-tooltip; pointer-events: none; + max-width: calc(100vw - 16px); } .tooltip-container { display: flex; align-items: center; gap: 8px; + box-sizing: border-box; width: max-content; - max-width: var(--tooltip-max-width, 300px); + /* Default text tooltips have a readable compact width */ + max-width: var(--tooltip-max-width, min(360px, calc(100vw - 24px))); background-color: var(--tooltip-glass-bg, var(--color-surface)); color: var(--text-color); padding: 6px 12px; @@ -30,6 +33,12 @@ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + &.has-template { + /* Template tooltips can expand to fit their rich content & helpers while bounded safely */ + max-width: var(--tooltip-max-width, min(480px, calc(100vw - 24px))); + align-items: stretch; + } + &.top { transform: translateY(4px) scale(0.97); transform-origin: bottom center; @@ -63,5 +72,18 @@ overflow-wrap: break-word; word-break: normal; text-align: left; + + &.w-100 { + display: block; + width: 100%; + } +} + +.shortcut-combo { + display: inline-flex; + align-items: center; + gap: 3px; + flex-shrink: 0; + white-space: nowrap; } From bf7d6c1c2397061ed2cfb0994db4681380b17e1b Mon Sep 17 00:00:00 2001 From: Mika Pons Date: Fri, 18 Sep 2026 12:36:06 +0200 Subject: [PATCH 02/23] =?UTF-8?q?=E2=9C=A8=20Add=20student=20repository=20?= =?UTF-8?q?navigation=20and=20tooltip=20feedback=20in=20students=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graphs/students/students.component.html | 24 +++++ .../graphs/students/students.component.scss | 57 ++++++++++ .../graphs/students/students.component.ts | 101 ++++++++++++------ 3 files changed, 151 insertions(+), 31 deletions(-) diff --git a/src/app/components/graphs/students/students.component.html b/src/app/components/graphs/students/students.component.html index 66832b8a..b8363eb6 100644 --- a/src/app/components/graphs/students/students.component.html +++ b/src/app/components/graphs/students/students.component.html @@ -137,3 +137,27 @@
-
+
-
- - - - - +
- +
- -
+ +
- -
- -
- - +
@@ -341,29 +339,48 @@
{{ 'HOME.FILTER.YEAR' | translate }}
[disabled]="!isGithubConnected" [appTooltip]="(!isGithubConnected ? ('HOME.MUST-LOGIN-GITHUB' | translate) : '')" placement="left" - (click)="createAssignment('github', $event)"> + (click)="createAssignment('github', 'github.com', undefined, $event)"> GitHub - + GitLab - + GitLab (gitlab.com) + + + +
@@ -401,7 +418,9 @@
{{ 'HOME.FILTER.YEAR' | translate }}
'is-editing': editingAssignmentId === assignment.id, 'is-shrunk': (hoveredAssignment === assignment.id || isSelected(assignment.id)) && editingAssignmentId !== assignment.id, 'status-preview-dimmed': hoveredStatusPreview && assignment.id !== -1 && assignment.uiStatus !== hoveredStatusPreview - }"> + }" + [appTooltip]="!isAssignmentConnected(assignment) && assignment.id !== -1 ? getDisconnectedTooltip(assignment) : null" + placement="top">
@@ -413,6 +432,11 @@
{{ 'HOME.FILTER.YEAR' | translate }}
+ + + + +
@@ -516,18 +540,18 @@
{{ 'HOME.FILTER.YEAR' | translate }}
diff --git a/src/app/components/home/assignment-chooser/assignment-chooser.component.scss b/src/app/components/home/assignment-chooser/assignment-chooser.component.scss index 45cc7a01..131eb208 100644 --- a/src/app/components/home/assignment-chooser/assignment-chooser.component.scss +++ b/src/app/components/home/assignment-chooser/assignment-chooser.component.scss @@ -6,6 +6,7 @@ .filter-group { display: inline-flex; + align-items: center; background: var(--filter-bg); border-radius: 10px; padding: 4px; @@ -21,22 +22,19 @@ background: var(--filter-indicator); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); border-radius: 6px; - transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); - z-index: -1; + transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), width 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); + z-index: 0; + pointer-events: none; } - &[data-active="all"] .filter-indicator { transform: translateX(0); } - - &[data-active="github"] .filter-indicator { transform: translateX(100px); } - - &[data-active="gitlab"] .filter-indicator { transform: translateX(200px); } - .filter-btn { + position: relative; + z-index: 1; background: transparent !important; box-shadow: none !important; border: none; - width: 100px; - padding: 0.5rem 0; + min-width: 100px; + padding: 0.5rem 0.75rem; text-align: center; font-size: 0.85rem; font-weight: 500; @@ -44,6 +42,11 @@ cursor: pointer; border-radius: 6px; transition: color 0.2s ease; + user-select: none; + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; &:hover:not(.active) { color: var(--color-text-primary); @@ -268,6 +271,23 @@ align-items: center; justify-content: center; line-height: 1; + + .custom-gitlab-icon-wrap { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + + .sub-server-icon { + position: absolute; + bottom: -2px; + right: -3px; + width: 8px; + height: 8px; + color: currentColor; + opacity: 0.9; + } + } } .split-text { @@ -361,6 +381,25 @@ flex-shrink: 0; } + .custom-gitlab-dropdown-icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + flex-shrink: 0; + + .sub-server-svg { + position: absolute; + bottom: -2px; + right: -3px; + width: 8px; + height: 8px; + color: var(--color-text-secondary); + } + } + .provider-name { flex: 1; white-space: nowrap; @@ -550,6 +589,42 @@ } .filter-dropdown-container { + .btn-outline-ghost { + box-sizing: border-box; + height: 34px; + min-width: 34px; + padding: 0; + border-radius: 8px; + border: 1px solid transparent; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background-color 240ms ease, + border-color 240ms ease, + box-shadow 240ms ease, + transform 150ms cubic-bezier(0.2, 0.8, 0.2, 1), + color 240ms ease; + + &:not(.has-active-filters) { + width: 34px; + } + + &.has-active-filters { + width: auto; + padding: 0 8px; + gap: 6px; + } + + &:active { + transform: scale(0.94); + } + + &:hover:not([aria-expanded="true"]) { + background-color: var(--filter-bg); + color: var(--color-text-primary); + } + } + .filter-badge { background-color: var(--filter-bg); color: var(--color-text-primary); @@ -557,7 +632,7 @@ padding: 0.1rem 0.4rem; border-radius: 99px; font-weight: 600; - margin-left: 0.5rem; + margin-left: 0.25rem; } } @@ -796,6 +871,27 @@ &.github .fa-github { color: var(--color-github, #24292e); } &.gitlab .fa-gitlab { color: var(--color-gitlab, #fc6d26); } + + .custom-gitlab-card-badge { + position: absolute; + bottom: -1px; + right: -1px; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + color: var(--color-text-secondary); + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-sm); + + .card-server-svg { + width: 7px; + height: 7px; + } + } } .selection-checkbox-overlay { diff --git a/src/app/components/home/assignment-chooser/assignment-chooser.component.ts b/src/app/components/home/assignment-chooser/assignment-chooser.component.ts index f4db7abb..5b6e2341 100644 --- a/src/app/components/home/assignment-chooser/assignment-chooser.component.ts +++ b/src/app/components/home/assignment-chooser/assignment-chooser.component.ts @@ -1,4 +1,16 @@ -import { Component, OnInit, TemplateRef, ViewChild, ChangeDetectorRef, OnDestroy, HostListener, ElementRef } from "@angular/core"; +import { + Component, + OnInit, + AfterViewInit, + TemplateRef, + ViewChild, + ViewChildren, + QueryList, + ChangeDetectorRef, + OnDestroy, + HostListener, + ElementRef, +} from "@angular/core"; import { Router } from "@angular/router"; import { Assignment } from "@models/Assignment.model"; import { GitProviderType } from "@models/GitAuthProvider.model"; @@ -14,20 +26,33 @@ import { ToastService } from "@services/toast.service"; import { Subscription } from "rxjs"; import * as moment from "moment"; +export interface ProviderFilterPill { + key: string; + label: string; + provider?: GitProviderType; + instanceHost?: string; + instanceName?: string; + isCustom?: boolean; +} + @Component({ selector: "assignment-chooser", templateUrl: "./assignment-chooser.component.html", styleUrls: ["./assignment-chooser.component.scss"], }) -export class AssignmentChooserComponent implements OnInit, OnDestroy { +export class AssignmentChooserComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild("searchInput") searchInputRef?: ElementRef; + @ViewChildren("pillBtn") pillButtons?: QueryList>; + + indicatorLeft = 0; + indicatorWidth = 100; assignments: any[]; // Using any to attach UI-specific properties temporarily sortField = "lastModificationDate"; sortDirection: "asc" | "desc" = "desc"; - filterType: "all" | "github" | "gitlab" = "all"; + filterType = "all"; searchQuery = ""; searchFocused = false; @@ -58,6 +83,8 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { // Provider split button dropdown state isProviderDropdownOpen = false; lastUsedProvider: GitProviderType = "github"; + lastUsedHost = "github.com"; + lastUsedInstanceName = ""; // Inline edit state editingAssignmentId: number | null = null; @@ -69,6 +96,7 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { private dbSubscription?: Subscription; private overlaySub: Subscription | null = null; private accountsSub?: Subscription; + private pillButtonsSub?: Subscription; private statusPreviewTimeout: any = null; constructor( @@ -101,19 +129,152 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { this.closeAllCreationPopovers(); } + @HostListener("window:resize") + onWindowResize(): void { + this.updateIndicator(); + } + get isSearchExpanded(): boolean { return this.searchFocused || Boolean(this.searchQuery && this.searchQuery.trim().length > 0); } + get providerFilterPills(): ProviderFilterPill[] { + const pills: ProviderFilterPill[] = [{ key: "all", label: "HOME.FILTER.ALL" }]; + + const hasGithub = + this.accountsService.isGithubConnected || (this.assignments && this.assignments.some((a) => !a.provider || a.provider === "github")); + if (hasGithub) { + pills.push({ + key: "github", + label: "HOME.FILTER.GITHUB", + provider: "github", + instanceHost: "github.com", + }); + } + + const hasGitlabCloud = + this.accountsService.isGitlabConnected || + (this.assignments && + this.assignments.some( + (a) => a.provider === "gitlab" && (!a.instanceHost || a.instanceHost === "gitlab.com" || a.resolvedInstanceHost === "gitlab.com"), + )); + if (hasGitlabCloud) { + pills.push({ + key: "gitlab", + label: "HOME.FILTER.GITLAB", + provider: "gitlab", + instanceHost: "gitlab.com", + }); + } + + const customHostMap = new Map(); + + const connectedCustom = this.accountsService.gitlabCustomAuthService?.getAccounts() || []; + connectedCustom.forEach((acc) => { + if (acc.instanceHost) { + customHostMap.set(acc.instanceHost, { + name: acc.instanceName || this.cleanHostname(acc.instanceHost), + isConnected: true, + }); + } + }); + + if (this.assignments) { + this.assignments.forEach((a) => { + const host = a.instanceHost || (a.provider === "gitlab" && a.resolvedInstanceHost !== "gitlab.com" ? a.resolvedInstanceHost : null); + if (a.provider === "gitlab" && host && host !== "gitlab.com" && !customHostMap.has(host)) { + customHostMap.set(host, { + name: a.instanceName || this.cleanHostname(host), + isConnected: this.accountsService.hasAccountForHost("gitlab", host), + }); + } + }); + } + + customHostMap.forEach((info, host) => { + pills.push({ + key: `gitlab:${host}`, + label: `GitLab (${info.name})`, + provider: "gitlab", + instanceHost: host, + instanceName: info.name, + isCustom: true, + }); + }); + + return pills; + } + + get selectedFilterPill(): ProviderFilterPill | undefined { + return this.providerFilterPills.find((p) => p.key === this.filterType); + } + + get isCustomLastUsed(): boolean { + return this.lastUsedProvider === "gitlab" && Boolean(this.lastUsedHost && this.lastUsedHost !== "gitlab.com"); + } + + get canCreateLastUsed(): boolean { + return this.accountsService.hasAccountForHost(this.lastUsedProvider, this.lastUsedHost); + } + + get createButtonLabel(): string { + if (this.lastUsedProvider === "github") { + return this.translateService.instant("HOME.CREATE-ASSIGNMENT-GITHUB"); + } + if (this.lastUsedProvider === "gitlab") { + if (this.isCustomLastUsed) { + return this.translateService.instant("HOME.CREATE-ASSIGNMENT-GITLAB-CUSTOM", { + host: this.lastUsedInstanceName || this.lastUsedHost, + }); + } + return this.translateService.instant("HOME.CREATE-ASSIGNMENT-GITLAB"); + } + return this.translateService.instant("HOME.CREATE-ASSIGNMENT"); + } + + get createButtonTooltip(): string { + if (!this.canCreateLastUsed) { + if (this.lastUsedProvider === "gitlab") { + if (this.isCustomLastUsed) { + return this.translateService.instant("HOME.MUST-LOGIN-ASSIGNMENT-GITLAB-CUSTOM", { + host: this.lastUsedInstanceName || this.lastUsedHost, + }); + } + return this.translateService.instant("HOME.MUST-LOGIN-GITLAB"); + } + return this.translateService.instant("HOME.MUST-LOGIN-GITHUB"); + } + return ""; + } + + get connectedCustomGitlabAccounts(): any[] { + return this.accountsService.gitlabCustomAuthService?.getAccounts() || []; + } + get filteredAssignments() { let result = this.assignments; // Always show the assignment currently being created at the top, even if it doesn't match filters const newAssignment = this.assignments.find((a) => a.id === -1); - // Apply old filterType + // Apply filterType if (this.filterType !== "all") { - result = result.filter((a) => (a as any).uiType === this.filterType || a.id === -1); + if (this.filterType === "github") { + result = result.filter((a) => a.id === -1 || !a.provider || a.provider === "github"); + } else if (this.filterType === "gitlab" || this.filterType === "gitlab-cloud") { + result = result.filter( + (a) => + a.id === -1 || + (a.provider === "gitlab" && (!a.instanceHost || a.instanceHost === "gitlab.com" || a.resolvedInstanceHost === "gitlab.com")), + ); + } else if (this.filterType.startsWith("gitlab:")) { + const targetHost = this.filterType.substring(7); + result = result.filter( + (a) => a.id === -1 || (a.provider === "gitlab" && (a.instanceHost === targetHost || a.resolvedInstanceHost === targetHost)), + ); + } else { + result = result.filter((a) => (a as any).uiType === this.filterType || a.id === -1); + } } // Apply search query @@ -236,8 +397,84 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { } } - setFilter(type: "all" | "github" | "gitlab") { + setFilter(type: string): void { this.filterType = type; + this.updateIndicator(); + this.cdr.markForCheck(); + } + + updateIndicator(): void { + if (!this.pillButtons || this.pillButtons.length === 0) { + return; + } + const pills = this.providerFilterPills; + let activeIndex = pills.findIndex((p) => p.key === this.filterType); + if (activeIndex === -1) { + activeIndex = 0; + } + const buttonsArray = this.pillButtons.toArray(); + const targetBtn = buttonsArray[activeIndex]; + if (targetBtn && targetBtn.nativeElement) { + const el = targetBtn.nativeElement; + const groupEl = el.parentElement; + const groupPaddingLeft = groupEl ? parseFloat(getComputedStyle(groupEl).paddingLeft) || 4 : 4; + this.indicatorLeft = Math.max(0, el.offsetLeft - groupPaddingLeft); + this.indicatorWidth = el.offsetWidth || 100; + this.cdr.markForCheck(); + } + } + + isAssignmentConnected(assignment: any): boolean { + if (!assignment) { + return false; + } + const provider = (assignment.provider || assignment.uiType || "github") as GitProviderType; + const host = assignment.instanceHost || (assignment.resolvedInstanceHost ? assignment.resolvedInstanceHost : undefined); + return this.accountsService.hasAccountForHost(provider, host); + } + + isCustomGitlab(assignment: any): boolean { + if (!assignment) { + return false; + } + const provider = assignment.provider || assignment.uiType; + const host = assignment.instanceHost || assignment.resolvedInstanceHost; + return provider === "gitlab" && Boolean(host && host !== "gitlab.com"); + } + + getAssignmentTooltip(assignment: any): string { + if (this.isAssignmentConnected(assignment)) { + return "HOME.SELECT-ASSIGNMENT-TOOLTIP"; + } + return this.getDisconnectedTooltip(assignment); + } + + getAssignmentEditTooltip(assignment: any): string { + if (this.isAssignmentConnected(assignment)) { + return "HOME.EDIT-ASSIGNMENT-TOOLTIP"; + } + return this.getDisconnectedTooltip(assignment); + } + + getAssignmentDeleteTooltip(assignment: any): string { + if (this.isAssignmentConnected(assignment)) { + return "HOME.DELETE-ASSIGNMENT-TOOLTIP"; + } + return this.getDisconnectedTooltip(assignment); + } + + getDisconnectedTooltip(assignment: any): string { + const provider = assignment?.provider || "github"; + const host = assignment?.instanceHost || assignment?.resolvedInstanceHost; + if (provider === "gitlab") { + if (host && host !== "gitlab.com") { + return this.translateService.instant("HOME.MUST-LOGIN-ASSIGNMENT-GITLAB-CUSTOM", { + host: assignment?.instanceName || host, + }); + } + return this.translateService.instant("HOME.MUST-LOGIN-ASSIGNMENT-GITLAB"); + } + return this.translateService.instant("HOME.MUST-LOGIN-ASSIGNMENT-GITHUB"); } onStatusMouseEnter(status: string) { @@ -298,14 +535,6 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { this.cdr.markForCheck(); } - isAssignmentConnected(assignment: any): boolean { - if (!assignment) { - return false; - } - const provider = (assignment.provider || assignment.uiType || "github") as GitProviderType; - return this.accountsService.hasAccount(provider); - } - get isGithubConnected(): boolean { return this.accountsService.isGithubConnected; } @@ -340,9 +569,28 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { this.accountsSub = this.accountsService.accounts$.subscribe(() => { this.cdr.markForCheck(); + requestAnimationFrame(() => { + this.updateIndicator(); + }); }); } + ngAfterViewInit(): void { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + this.updateIndicator(); + }); + }); + + if (this.pillButtons) { + this.pillButtonsSub = this.pillButtons.changes.subscribe(() => { + requestAnimationFrame(() => { + this.updateIndicator(); + }); + }); + } + } + ngOnDestroy(): void { if (this.dbSubscription) { this.dbSubscription.unsubscribe(); @@ -353,6 +601,9 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { if (this.accountsSub) { this.accountsSub.unsubscribe(); } + if (this.pillButtonsSub) { + this.pillButtonsSub.unsubscribe(); + } if (this.statusPreviewTimeout) { clearTimeout(this.statusPreviewTimeout); } @@ -374,6 +625,14 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { if (savedProvider && (savedProvider === "github" || savedProvider === "gitlab")) { this.lastUsedProvider = savedProvider; } + const savedHost = localStorage.getItem("git4school_last_host"); + if (savedHost) { + this.lastUsedHost = savedHost; + } + const savedName = localStorage.getItem("git4school_last_instance_name"); + if (savedName) { + this.lastUsedInstanceName = savedName; + } } savePreferences() { @@ -426,6 +685,8 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { const lastAssignment = this.assignments.find((a) => a.provider && a.id !== -1); if (lastAssignment && lastAssignment.provider) { this.lastUsedProvider = lastAssignment.provider; + this.lastUsedHost = lastAssignment.instanceHost || (lastAssignment.provider === "gitlab" ? "gitlab.com" : "github.com"); + this.lastUsedInstanceName = lastAssignment.instanceName || ""; } } @@ -434,6 +695,9 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { this.assignments.unshift(newAssignmentObj); } this.cdr.detectChanges(); + requestAnimationFrame(() => { + this.updateIndicator(); + }); }); } @@ -696,9 +960,9 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { selectAssignment(assignment: any) { const provider = assignment.provider || "github"; - if (!this.accountsService.hasAccount(provider)) { - const errorKey = provider === "gitlab" ? "HOME.MUST-LOGIN-ASSIGNMENT-GITLAB" : "HOME.MUST-LOGIN-ASSIGNMENT-GITHUB"; - const msg = this.translateService.instant(errorKey); + const host = assignment.instanceHost || (assignment.resolvedInstanceHost ? assignment.resolvedInstanceHost : undefined); + if (!this.accountsService.hasAccountForHost(provider, host)) { + const msg = this.getDisconnectedTooltip(assignment); this.toastService.warning(this.translateService.instant("WARNING"), msg); return; } @@ -710,24 +974,39 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { } deleteAssignment(assignment: any) { - if (!this.isAssignmentConnected(assignment)) return; + if (!this.isAssignmentConnected(assignment)) { + const msg = this.getDisconnectedTooltip(assignment); + this.toastService.warning(this.translateService.instant("WARNING"), msg); + return; + } this.databaseService.deleteAssignment(assignment.id); } - createAssignment(provider?: GitProviderType, event?: MouseEvent) { + createAssignment(provider?: GitProviderType, instanceHost?: string, instanceName?: string, event?: MouseEvent) { if (event) { event.stopPropagation(); } - const targetProvider: GitProviderType = provider || (this.filterType !== "all" ? this.filterType : this.lastUsedProvider) || "github"; + const targetProvider: GitProviderType = provider || (this.filterType === "github" ? "github" : this.lastUsedProvider) || "github"; + const targetHost = instanceHost || (targetProvider === "gitlab" ? this.lastUsedHost || "gitlab.com" : "github.com"); - if (!this.accountsService.hasAccount(targetProvider)) { + if (!this.accountsService.hasAccountForHost(targetProvider, targetHost)) { if (this.accountsService.isEmpty()) { const msg = this.translateService.instant("HOME.MUST-LOGIN"); this.toastService.warning(this.translateService.instant("WARNING"), msg); return; } - const errorKey = targetProvider === "gitlab" ? "HOME.MUST-LOGIN-GITLAB" : "HOME.MUST-LOGIN-GITHUB"; - const msg = this.translateService.instant(errorKey); + let msg = ""; + if (targetProvider === "gitlab") { + if (targetHost !== "gitlab.com") { + msg = this.translateService.instant("HOME.MUST-LOGIN-ASSIGNMENT-GITLAB-CUSTOM", { + host: instanceName || targetHost, + }); + } else { + msg = this.translateService.instant("HOME.MUST-LOGIN-GITLAB"); + } + } else { + msg = this.translateService.instant("HOME.MUST-LOGIN-GITHUB"); + } this.toastService.warning(this.translateService.instant("WARNING"), msg); return; } @@ -736,13 +1015,25 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { this.closeAllCreationPopovers(); this.lastUsedProvider = targetProvider; + this.lastUsedHost = targetHost; + this.lastUsedInstanceName = instanceName || ""; try { localStorage.setItem("git4school_last_provider", targetProvider); + localStorage.setItem("git4school_last_host", targetHost); + if (instanceName) { + localStorage.setItem("git4school_last_instance_name", instanceName); + } else { + localStorage.removeItem("git4school_last_instance_name"); + } } catch (e) {} let assignment = new Assignment(); assignment.id = -1; // Temporary ID for creation assignment.provider = targetProvider; + assignment.instanceHost = targetHost; + if (instanceName) { + assignment.instanceName = instanceName; + } (assignment as any).uiType = targetProvider; (assignment as any).uiStatus = "prepared"; this.assignments.unshift(assignment); // Add to the top @@ -761,17 +1052,12 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { return; } - /* If a specific provider filter is active, create directly with that provider */ - if (this.filterType === "github") { - this.createAssignment("github"); - return; - } - if (this.filterType === "gitlab") { - this.createAssignment("gitlab"); + if (this.selectedFilterPill && this.selectedFilterPill.key !== "all") { + this.createAssignment(this.selectedFilterPill.provider, this.selectedFilterPill.instanceHost, this.selectedFilterPill.instanceName); return; } - this.createAssignment(this.lastUsedProvider); + this.createAssignment(this.lastUsedProvider, this.lastUsedHost, this.lastUsedInstanceName); } toggleProviderDropdown(event?: MouseEvent): void { @@ -790,6 +1076,10 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { return item?.id !== undefined ? item.id : index; } + trackByPillKey(index: number, pill: ProviderFilterPill): string { + return pill.key; + } + editAssignment(assignment: any) { if (!this.isAssignmentConnected(assignment) && assignment.id !== -1) return; @@ -824,8 +1114,16 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { onAssignmentSaved(assignment: Assignment) { if (assignment && assignment.provider) { this.lastUsedProvider = assignment.provider; + this.lastUsedHost = assignment.instanceHost || (assignment.provider === "gitlab" ? "gitlab.com" : "github.com"); + this.lastUsedInstanceName = assignment.instanceName || ""; try { localStorage.setItem("git4school_last_provider", assignment.provider); + localStorage.setItem("git4school_last_host", this.lastUsedHost); + if (assignment.instanceName) { + localStorage.setItem("git4school_last_instance_name", assignment.instanceName); + } else { + localStorage.removeItem("git4school_last_instance_name"); + } } catch (e) {} } this.isCreatingNew = false; @@ -862,4 +1160,12 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { this.importDB(file); } } + + private cleanHostname(host: string): string { + return host + .replace(/^https?:\/\//, "") + .replace(/^www\./, "") + .replace(/^gitlab\./, "") + .replace(/\/$/, ""); + } } diff --git a/src/app/components/home/assignment-chooser/configuration/edit-repositories/edit-repositories.component.ts b/src/app/components/home/assignment-chooser/configuration/edit-repositories/edit-repositories.component.ts index ced73f68..101137c6 100644 --- a/src/app/components/home/assignment-chooser/configuration/edit-repositories/edit-repositories.component.ts +++ b/src/app/components/home/assignment-chooser/configuration/edit-repositories/edit-repositories.component.ts @@ -51,7 +51,7 @@ export class EditRepositoriesComponent extends BaseTabEditConfigurationComponent } get isConnectedToProvider(): boolean { - return this.accountsService.hasAccount(this.provider); + return this.accountsService.hasAccountForHost(this.provider, this.assignment?.instanceHost || this.assignment?.resolvedInstanceHost); } private rotateMatrix: { [key: string]: SortDirection } = { @@ -288,6 +288,7 @@ export class EditRepositoriesComponent extends BaseTabEditConfigurationComponent let modalReference: CustomModalRef = this.modalService.open(ModalAddRepositoriesComponent, { size: "lg" }); modalReference.componentInstance.repoList = this.getFormControls.map((row) => Repository.withJSON(row.value)); modalReference.componentInstance.provider = this.provider; + modalReference.componentInstance.instanceHost = this.assignment?.instanceHost || this.assignment?.resolvedInstanceHost; modalReference.result.then( (result) => { diff --git a/src/app/components/home/assignment-chooser/configuration/edit-repositories/modal-add-repositories/modal-add-repositories.component.ts b/src/app/components/home/assignment-chooser/configuration/edit-repositories/modal-add-repositories/modal-add-repositories.component.ts index d5d455ef..da43eb9f 100644 --- a/src/app/components/home/assignment-chooser/configuration/edit-repositories/modal-add-repositories/modal-add-repositories.component.ts +++ b/src/app/components/home/assignment-chooser/configuration/edit-repositories/modal-add-repositories/modal-add-repositories.component.ts @@ -16,6 +16,7 @@ export class ModalAddRepositoriesComponent implements OnInit, OnDestroy, AfterVi @ViewChild("reposTable", { read: ElementRef }) datatable: ElementRef; @Input() repoList: Repository[]; @Input() provider: GitProviderType = "github"; + @Input() instanceHost?: string; rows: Repository[]; nameMatches: Repository[] = []; contentMatches: Repository[] = []; @@ -304,11 +305,17 @@ export class ModalAddRepositoriesComponent implements OnInit, OnDestroy, AfterVi } private updateResultsWithAuthenticatedUser(cursor?: string, isFirstPage = false) { - this.processIntermediateResponse(this.commitsService.getRepositoriesByAuthenticatedUser(cursor, 100, this.provider), isFirstPage); + this.processIntermediateResponse( + this.commitsService.getRepositoriesByAuthenticatedUser(cursor, 100, this.provider, this.instanceHost), + isFirstPage, + ); } private updateResultsWithSearchFilter(searchFilter: string, cursor?: string, isFirstPage = false) { - this.processIntermediateResponse(this.commitsService.getRepositoriesBySearch(searchFilter, cursor, 100, this.provider), isFirstPage); + this.processIntermediateResponse( + this.commitsService.getRepositoriesBySearch(searchFilter, cursor, 100, this.provider, this.instanceHost), + isFirstPage, + ); } private loadResults() { diff --git a/src/app/components/nav-layouts/sidebar-settings/accounts/add-account-modal/add-account-modal.component.html b/src/app/components/nav-layouts/sidebar-settings/accounts/add-account-modal/add-account-modal.component.html index 33ae8ae1..1ba9d327 100644 --- a/src/app/components/nav-layouts/sidebar-settings/accounts/add-account-modal/add-account-modal.component.html +++ b/src/app/components/nav-layouts/sidebar-settings/accounts/add-account-modal/add-account-modal.component.html @@ -35,7 +35,7 @@
@@ -137,7 +168,12 @@
- {{ (selectedPlatform === 'github' ? 'ACCOUNTS.PLATFORM_GITHUB_TITLE' : (selectedPlatform === 'gitlab-cloud' ? 'ACCOUNTS.PLATFORM_GITLAB_TITLE' : 'ACCOUNTS.PLATFORM_GITLAB_SELF_TITLE')) | translate }} + {{ 'ACCOUNTS.PLATFORM_GITHUB_TITLE' | translate }} + {{ 'ACCOUNTS.PLATFORM_GITLAB_TITLE' | translate }} + + GitLab ({{ acc.instanceName || acc.instanceHost }}) + {{ 'ACCOUNTS.PLATFORM_GITLAB_SELF_TITLE' | translate }} + {{ platformAuthTypeKey | translate }} @@ -155,7 +191,7 @@