From 3d5a9462bc82b1fc95c8aa8d99ad50f4b6a84fe7 Mon Sep 17 00:00:00 2001 From: Matt Gunter Date: Wed, 4 Dec 2024 12:20:21 -0500 Subject: [PATCH] Implement member retrieval by login, enhance recent surveys query, and add kudos update functionality --- backend/package.json | 2 +- backend/src/controllers/survey.controller.ts | 22 ++- backend/src/controllers/teams.controller.ts | 18 ++ backend/src/database.ts | 2 +- backend/src/routes/index.ts | 2 + backend/src/services/survey.service.ts | 11 +- .../new-copilot-survey.component-copy.scss | 165 ++++++++++++++++++ .../new-copilot-survey.component.html | 86 ++++++--- .../new-copilot-survey.component.scss | 81 ++++++++- .../new-copilot-survey.component.ts | 164 ++++++++++------- .../services/api/copilot-survey.service.ts | 10 +- .../src/app/services/api/members.service.ts | 10 ++ 12 files changed, 473 insertions(+), 100 deletions(-) create mode 100644 frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component-copy.scss diff --git a/backend/package.json b/backend/package.json index 609b0d2..865ee0a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,7 @@ "start": "node --enable-source-maps dist/index.js | bunyan -o short -l info", "test": "jest", "build": "tsc", - "dev": "tsx watch src/index.ts | bunyan -o short -l debug", + "dev": "tsx watch src/index.ts | bunyan -o short -l info", "lint": "eslint src/**/*.ts", "db:start": "docker-compose -f ../compose.yml up -d db", "dotenv": "cp -n .env.example .env || true" diff --git a/backend/src/controllers/survey.controller.ts b/backend/src/controllers/survey.controller.ts index 898c144..a6c7f27 100644 --- a/backend/src/controllers/survey.controller.ts +++ b/backend/src/controllers/survey.controller.ts @@ -35,7 +35,7 @@ class SurveyController { async getRecentSurveysWithGoodReasons(req: Request, res: Response): Promise { try { - const minReasonLength = parseInt(req.query.minReasonLength as string) || 20; + const minReasonLength = parseInt(req.params.minReasonLength, 10) || 20; const surveys = await surveyService.getRecentSurveysWithGoodReasons(minReasonLength); res.status(200).json(surveys); } catch (error) { @@ -120,6 +120,26 @@ class SurveyController { } } + async updateKudos(req: Request, res: Response): Promise { + try { + const { id } = req.params; + + const survey = await Survey.findByPk(id); + if (!survey) { + res.status(404).json({ error: 'Survey not found' }); + return; + } + + survey.kudos = (survey.kudos || 0) + 1; + await survey.save(); + + res.status(200).json(survey); + } catch (error) { + console.log(error); + res.status(500).json(error); + } + } + async deleteSurvey(req: Request, res: Response): Promise { try { const { id } = req.params; diff --git a/backend/src/controllers/teams.controller.ts b/backend/src/controllers/teams.controller.ts index e753258..f2d7cf7 100644 --- a/backend/src/controllers/teams.controller.ts +++ b/backend/src/controllers/teams.controller.ts @@ -58,6 +58,24 @@ class TeamsController { res.status(500).json(error); } } + + async getMemberByLogin(req: Request, res: Response): Promise { + try { + const { login } = req.params; + const member = await Member.findOne({ + where: { login }, + attributes: ['login', 'name', 'url', 'avatar_url'] + }); + + if (member) { + res.json(member); + } else { + res.status(404).json({ message: 'User not found' }); + } + } catch (error) { + res.status(500).json(error); + } + } } export default new TeamsController(); \ No newline at end of file diff --git a/backend/src/database.ts b/backend/src/database.ts index dc7f7cc..6b0c9e5 100644 --- a/backend/src/database.ts +++ b/backend/src/database.ts @@ -17,7 +17,7 @@ class Database { sequelize?: Sequelize; options: Options = { dialect: 'mysql', - logging: (sql) => logger.debug(sql), + logging: (sql) => logger.error(sql), timezone: '+00:00', // Force UTC timezone dialectOptions: { timezone: '+00:00' // Force UTC for MySQL connection diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index ec9bd9e..4c6d818 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -21,6 +21,7 @@ router.put('/survey/:id', surveyController.updateSurvey); // put github survey l router.delete('/survey/:id', surveyController.deleteSurvey); router.get('/survey/recent-good-reasons/:minReasonLength', surveyController.getRecentSurveysWithGoodReasons); router.put('/survey/:id/github', surveyController.updateSurveyGitHub); +router.put('/survey/kudos/:id', surveyController.updateKudos); router.get('/usage', usageController.getUsage); @@ -34,6 +35,7 @@ router.get('/seats/:id', SeatsController.getSeat); router.get('/teams', teamsController.getAllTeams); router.get('/members', teamsController.getAllMembers); +router.get('/members/:login', teamsController.getMemberByLogin); router.get('/settings', settingsController.getAllSettings); router.post('/settings', settingsController.createSettings); diff --git a/backend/src/services/survey.service.ts b/backend/src/services/survey.service.ts index efd2950..f98452e 100644 --- a/backend/src/services/survey.service.ts +++ b/backend/src/services/survey.service.ts @@ -13,17 +13,20 @@ class SurveyService { return await Survey.findByPk(survey.id); } - async getRecentSurveysWithGoodReasons(minReasonLength: number = 20) { - return await Survey.findAll({ + + async getRecentSurveysWithGoodReasons(minReasonLength: number): Promise { + return Survey.findAll({ where: { reason: { [Op.and]: [ { [Op.ne]: null }, - { [Op.length]: { [Op.gt]: minReasonLength } } + { [Op.ne]: '' }, + { [Op.gte]: minReasonLength } ] } }, - order: [['updatedAt', 'DESC']] + order: [['updatedAt', 'DESC']], + limit: 20 }); } } diff --git a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component-copy.scss b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component-copy.scss new file mode 100644 index 0000000..acac553 --- /dev/null +++ b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component-copy.scss @@ -0,0 +1,165 @@ +/* Core Layout */ +.page-container { + max-width: 1000px; + margin: 0 auto; + padding: 1.5rem; + } + + .two-column-layout { + display: grid; + grid-template-columns: 3fr 2fr; + gap: 2rem; + align-items: start; + } + + /* Typography & Questions */ + .page-header { + margin-bottom: 2rem; + } + + .page-header h2 { + font-size: 1.75rem; + font-weight: 600; + color: #1a202c; + margin: 0; + } + + .page-header h4 { + font-size: 1rem; + color: #718096; + margin: 0.25rem 0 0; + } + + .conditional-fields { + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 1.25rem; + margin-bottom: 1.5rem; + } + + /* Numbered Questions */ + .question { + position: relative; + padding-left: 2rem; + margin: 1.5rem 0; + } + + .question::before { + content: attr(data-number); + position: absolute; + left: 0; + color: #4a5568; + font-weight: 500; + } + + /* Form Elements */ + .example-form-field { + width: 100%; + margin-bottom: 1rem; + } + + .reason-label { + font-size: 0.875rem; + color: #4a5568; + margin-top: 0.5rem; + } + + /* Radio & Slider */ + .example-radio-group { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .slider-labels-container { + width: 95%; + margin: 0 8px; + } + + /* Insights Card */ + .scrollable-card { + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + + .scrollable-card-content div { + padding: 0.75rem; + border-bottom: 1px solid #e2e8f0; + } + + /* Submit Button */ + button[type="submit"] { + width: 100%; + padding: 0.75rem; + font-size: 1rem; + margin-top: 1rem; + background: #4299e1; + color: white; + border-radius: 6px; + } + + .example-mat-slider { + width: 100%; +} + + +.slider-labels-container { + display: inline-block; + margin: 0 5px; + width: 90%; + .slider-labels { + display: block; + width: 90%; + height: 12px; + margin-top: -10px; + position: relative; + margin-bottom: 18px; + > * { + position: relative; + transform: translateX(-40%); + } + } +} + +.small-icon { + font-size: 20px; // Adjust the size as needed +} + + +@keyframes scroll { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-50%); + } +} + + +.scrollable-card { + max-height: 600px; /* Increase the maximum height for the card */ + overflow: hidden; /* Hide the scrollbar */ + white-space: normal; /* Ensure text wraps properly */ + position: relative; + margin-top: 20px; /* Add margin to separate from the header */ +} + +.scrollable-card-content { + position: relative; + animation: scroll 20s linear infinite; /* Adjust duration as needed */ + padding-top: 60px; /* Add padding to ensure content does not overlap with the title */ +} + +.scrollable-card-content:hover { + animation-play-state: paused; /* Pause animation on hover */ +} + +.scrollable-card-content div { + margin-bottom: 3em; /* Add extra space between reasons */ +} + +.kudos-count { + font-size: 0.9em; // Reduced by 2 points + margin-left: 5px; +} diff --git a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.html b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.html index 919b5ff..144addd 100644 --- a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.html +++ b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.html @@ -1,64 +1,96 @@
- +
+ + User ID + + + Invalid User ID + + + + Repository + + + + PR Number + + +
+
+
+ +
Yes No - +
+ + +
+ +
- +
0% - 25% + 10% + 20% + 30% + 40% 50% - 75% - 100% + 60%
- - If possible, Explain how copilot enabled that level of Time Savings - - -

- +
+
+ + + + + +
+ +
+ +
Faster PR's ๐Ÿš€ Faster Releases ๐Ÿ“ฆ Repo/Team Housekeeping ๐Ÿงน - Tech Debt, Reduce Defects and Vulns - ๐Ÿ› ๏ธ - Experiment, Learn, or Work on Something NEW - ๐Ÿงช + Tech Debt, Reduce Defects and Vulns ๐Ÿ› ๏ธ + Experiment, Learn, or Share Knowledge ๐Ÿงช Other ๐Ÿค” +
-
+

What colleagues are sharing...

"{{ reason }}" - thumb_up - - {{ surveys[i].kudos || 0 }} -
-
- "{{ reason }}" - - thumb_up + thumb_up {{ surveys[i].kudos || 0 }}
@@ -66,4 +98,4 @@

What colleagues are sharing...

-
``` \ No newline at end of file +
\ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.scss b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.scss index f9d0f94..712663f 100644 --- a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.scss +++ b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.scss @@ -11,6 +11,7 @@ .example-form-field { width: 100%; + margin-bottom: 20px; } .example-mat-slider { @@ -42,6 +43,7 @@ form { .two-column-layout { display: flex; justify-content: space-between; + gap: 20px; } .column { @@ -58,7 +60,7 @@ form { .scrollable-card-content { position: relative; - animation: scroll 30s linear infinite; /* Adjust duration as needed */ + animation: scroll 20s linear infinite; /* Adjust duration as needed */ padding-top: 60px; /* Add padding to ensure content does not overlap with the title */ } @@ -71,7 +73,7 @@ form { } .kudos-count { - font-size: 0.8em; + font-size: 0.9em; // Reduced by 2 points margin-left: 5px; } @@ -90,3 +92,78 @@ form { width: 100%; margin-bottom: 20px; /* Add margin to separate from the content */ } + +.conditional-fields { + border: 1px solid #ccc; + padding: 15px; + margin-bottom: 20px; + border-radius: 5px; + background-color: #f9f9f9; +} + +.label-large { + font-size: 1.5em; // Increase font size by 50% +} + +.small-icon { + font-size: 20px; // Adjust the size as needed +} + +.page-header { + text-align: center; + margin-bottom: 20px; +} + +.page-header h2 { + font-size: 2em; + margin: 0; +} + +.page-header h4 { + font-size: 1.2em; + margin: 0; + color: #666; +} + +.page-header h2, .page-header h4 { + margin: 0; + padding: 0; + display: block; +} + +.page-container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.page-header { + margin-bottom: 3rem; +} + +.conditional-fields { + padding: 2rem; + margin: 2rem 0; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.example-form-field { + margin-bottom: 1.5rem; +} + +.example-radio-group { + margin: 1.5rem 0; +} + +.scrollable-card { + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + transition: box-shadow 0.3s ease; +} + +.kudos-count { + transition: color 0.2s ease; +} + +.mat-slider-thumb { + transform: scale(1.2); +} \ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.ts b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.ts index 562e64f..e999ffe 100644 --- a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.ts +++ b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.ts @@ -1,8 +1,9 @@ -import { Component, forwardRef, OnInit } from '@angular/core'; +import { Component, forwardRef, OnInit, AfterViewInit } from '@angular/core'; import { AppModule } from '../../../../app.module'; import { FormBuilder, FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { CopilotSurveyService, Survey } from '../../../../services/api/copilot-survey.service'; import { ActivatedRoute, Params, Router } from '@angular/router'; +import { MembersService } from '../../../../services/api/members.service'; @Component({ selector: 'app-copilot-survey', @@ -18,18 +19,19 @@ import { ActivatedRoute, Params, Router } from '@angular/router'; } ], templateUrl: './new-copilot-survey.component.html', - styleUrl: './new-copilot-survey.component.scss' + styleUrl: './new-copilot-survey.component-copy.scss' }) -export class NewCopilotSurveyComponent implements OnInit { +export class NewCopilotSurveyComponent implements OnInit, AfterViewInit { surveyForm: FormGroup; params: Params = {}; defaultPercentTimeSaved = 25; id: number; + showConditionalFields = true; // Flag to show/hide conditional fields - initializationReason = 'I chose ' + this.defaultPercentTimeSaved + '% because Copilot enabled me to...'; + initializationReason = 'I estimated ' + this.defaultPercentTimeSaved + '% because Copilot enabled me to...'; didNotUsedCopilotReason = 'I did not use Copilot...'; - usedCopilotWithPercentTimeSaved = (percent: number) => `I chose ${percent}% because Copilot enabled me to...`; - usedCopilotWithPercentTimeSavedZero = 'I chose 0% because Copilot did not help me...'; + usedCopilotWithPercentTimeSaved = (percent: number) => `I estimated ${percent}% because Copilot enabled me to...`; + usedCopilotWithPercentTimeSavedZero = 'I estimated 0% because Copilot did not help me...'; formColumnWidth = 70; // Percentage width for the form column historyColumnWidth = 30; // Percentage width for the history column @@ -40,11 +42,16 @@ export class NewCopilotSurveyComponent implements OnInit { private fb: FormBuilder, private copilotSurveyService: CopilotSurveyService, private route: ActivatedRoute, - private router: Router + private router: Router, + private membersService: MembersService ) { const id = Number(this.route.snapshot.paramMap.get('id')); - this.id = isNaN(id) ? 0 : id + this.id = isNaN(id) ? 0 : id; + this.showConditionalFields = this.id === 0; this.surveyForm = this.fb.group({ + userId: new FormControl('', Validators.required), // Add userId field + repo: new FormControl(''), // Add repo field + prNumber: new FormControl(''), // Add prNumber field usedCopilot: new FormControl(true, Validators.required), percentTimeSaved: new FormControl(this.defaultPercentTimeSaved, Validators.required), reason: new FormControl(''), @@ -53,7 +60,14 @@ export class NewCopilotSurveyComponent implements OnInit { } ngOnInit() { - this.route.queryParams.subscribe(params => this.params = params); + this.route.queryParams.subscribe(params => { + this.params = params; + }); + + // Set scrollRestoration to auto + if ('scrollRestoration' in window.history) { + window.history.scrollRestoration = 'auto'; + } // Page Initialization this.setReasonDefault(); @@ -79,16 +93,19 @@ export class NewCopilotSurveyComponent implements OnInit { // Debugging: Confirm the reason list is not empty console.log('Historical Reasons:', this.historicalReasons); + } - // Duplicate the content dynamically - this.duplicateContent(); + ngAfterViewInit() { + // Restart the animation after the view is initialized + this.restartAnimation(); } - duplicateContent() { - const content = document.querySelector('.scrollable-card-content'); + restartAnimation() { + const content = document.querySelector('.scrollable-card-content') as HTMLElement; if (content) { - const clone = content.cloneNode(true); - content.appendChild(clone); + content.style.animation = 'none'; + content.offsetHeight; // Trigger reflow + content.style.animation = ''; } } @@ -114,59 +131,61 @@ export class NewCopilotSurveyComponent implements OnInit { promptUserForConfirmation() { // Implement the logic to prompt the user with a warning - alert("Confirm that reason and percentTimeSaved are correct."); + alert("Detected that reason and percentTimeSaved might be incorrect. Please confirm."); } loadHistoricalReasons() { - // Load historical reasons from the service - this.copilotSurveyService.getRecentSurveysWithGoodReasons(20).subscribe((surveys: Survey[]) => { - this.surveys = surveys; - this.historicalReasons = surveys.map(survey => survey.reason); - - // Debugging: Confirm the historical reasons are loaded - console.log('Historical Reasons:', this.historicalReasons); - if (this.historicalReasons.length > 0) { - console.log('First Reason:', this.historicalReasons[0]); - } - if (this.historicalReasons.length < 1) { - this.historicalReasons =[ - 'I chose 5% because Copilot enabled me to tidy up loose ends because I got added to pilot after I\'d already completed most of this task.', - 'I chose 10% because Copilot enabled me to make a very small change.', - 'I chose 10% because Copilot enabled me to make small familiar changes for which Copilot\'s main use was double-checking my work.', - 'I chose 10% because Copilot enabled me to connect files and models together, which Copilot isn\'t savvy on doing. It was helpful to figure out why a test wasn\'t working though.', - 'I chose 0% because this is my first time trying co-pilot so I expect this % to raise for future PR\'s.', - 'I chose 20% because Copilot enabled me to save time typing similar lines of code or docstrings.', - 'I chose 20% because Copilot chat asking how to use specific command. Gives faster accurate answer without having to dig in the CLI documentation.', - 'I chose 30% because Copilot helped with unit tests and writing boilerplate.', - 'I chose 30% because Copilot enabled me to save time writing comments and test-case boilerplate.', - 'I chose 30% because Copilot automatically adapted the code from previous lines so I barely wrote anything by hand.', - 'I chose 25% because I created a new nestJS service to provide Github App authentication to other services. I was adapting logic from another Shift-left service. My thinking behind the 21%-30% savings is that Copilot autocompleted several of the tie-ins to other parts of the application and felt to provide about that much productivity increase.', - 'I chose 20% because it was good when I had writer\'s block to suggest the next line, or what I might do. It was also very convenient to quickly search for an answer to a small question I had.', - 'I chose 20% because less typing required, prefilled code blocks for me.', - 'I chose 25% because for about half the work in this PR, I did the first quarter of it, told GH Copilot "see what I did at lines N through N in #file? Do that for...." and let it do the other one-quarter of the work for me.', - 'I chose 30% because Copilot was awesome for this. I used it for a combination of starting code blocks with comments and it made the function, and I used the inline chat. Compared to both it typing out stuff I knew how to do, and it finding solutions to stuff that would have taken time on the web, it felt like a really substantial time saver.', - 'I chose 30% because raw time spent coding and generating tests. Copilot was able to suggest more than 90% of the code I would have had to manually create.', - 'I chose 40% because Copilot reduced the amount of thought required.', - 'I chose 50% because it takes most of the time to write test cases for functionality. Copilot did it automatically for simple function with one input.', - 'I chose 45% because when scaffolding React components, there is much boilerplate that can be autofilled with Copilot. For example, in many cases, a small React component return value may be 10 lines. Copilot is often smart enough to autocomplete these components for me. The greatest gains are seen when scaffolding a brand new codebase. Gains are smaller if making small code changes or performing analysis.', - 'I chose 40% because Copilot assisted with formatting large sets of data for a test that normally would have taken much longer to do manually.', - 'I chose 50% because I was able to tab out the entire process.' - ]; - } + this.copilotSurveyService.getRecentSurveysWithGoodReasons(20).subscribe( + (surveys: Survey[]) => { + this.surveys = surveys; + this.historicalReasons = surveys.map(survey => survey.reason); - }); - + // Debugging: Confirm the historical reasons are loaded + console.log('Historical Reasons:', this.historicalReasons); + if (this.historicalReasons.length > 0) { + console.log('First Reason:', this.historicalReasons[0]); + } + if (this.historicalReasons.length < 1) { + this.historicalReasons =[ + 'I chose 5% because Copilot enabled me to tidy up loose ends because I got added to pilot after I\'d already completed most of this task.', + 'I chose 10% because Copilot enabled me to make a very small change.', + 'I chose 10% because Copilot enabled me to make small familiar changes for which Copilot\'s main use was double-checking my work.', + 'I chose 10% because Copilot enabled me to connect files and models together, which Copilot isn\'t savvy on doing. It was helpful to figure out why a test wasn\'t working though.', + 'I chose 0% because this is my first time trying co-pilot so I expect this % to raise for future PR\'s.', + 'I chose 20% because Copilot enabled me to save time typing similar lines of code or docstrings.', + 'I chose 20% because Copilot chat asking how to use specific command. Gives faster accurate answer without having to dig in the CLI documentation.', + 'I chose 30% because Copilot helped with unit tests and writing boilerplate.', + 'I chose 30% because Copilot enabled me to save time writing comments and test-case boilerplate.', + 'I chose 30% because Copilot automatically adapted the code from previous lines so I barely wrote anything by hand.', + 'I chose 25% because I created a new nestJS service to provide Github App authentication to other services. I was adapting logic from another Shift-left service. My thinking behind the 21%-30% savings is that Copilot autocompleted several of the tie-ins to other parts of the application and felt to provide about that much productivity increase.', + 'I chose 20% because it was good when I had writer\'s block to suggest the next line, or what I might do. It was also very convenient to quickly search for an answer to a small question I had.', + 'I chose 20% because less typing required, prefilled code blocks for me.', + 'I chose 25% because for about half the work in this PR, I did the first quarter of it, told GH Copilot "see what I did at lines N through N in #file? Do that for...." and let it do the other one-quarter of the work for me.', + 'I chose 30% because Copilot was awesome for this. I used it for a combination of starting code blocks with comments and it made the function, and I used the inline chat. Compared to both it typing out stuff I knew how to do, and it finding solutions to stuff that would have taken time on the web, it felt like a really substantial time saver.', + 'I chose 30% because raw time spent coding and generating tests. Copilot was able to suggest more than 90% of the code I would have had to manually create.', + 'I chose 40% because Copilot reduced the amount of thought required.', + 'I chose 50% because it takes most of the time to write test cases for functionality. Copilot did it automatically for simple function with one input.', + 'I chose 45% because when scaffolding React components, there is much boilerplate that can be autofilled with Copilot. For example, in many cases, a small React component return value may be 10 lines. Copilot is often smart enough to autocomplete these components for me. The greatest gains are seen when scaffolding a brand new codebase. Gains are smaller if making small code changes or performing analysis.', + 'I chose 40% because Copilot assisted with formatting large sets of data for a test that normally would have taken much longer to do manually.', + 'I chose 50% because I was able to tab out the entire process.' + ]; + } + + // Restart the animation after loading historical reasons + this.restartAnimation(); + }, + (error) => { + console.error('Error loading historical reasons:', error); + } + ); } addKudos(event: Event, index: number) { event.preventDefault(); const survey = this.surveys[index]; - if (survey) { - if (!survey.kudos) { - survey.kudos = 0; - } - survey.kudos += 1; - this.copilotSurveyService.updateSurvey(survey).subscribe(() => { + if (survey && survey.id !== undefined) { + this.copilotSurveyService.updateKudos(survey.id).subscribe(() => { + survey.kudos = (survey.kudos || 0) + 1; console.log(`Kudos added to survey with id ${survey.id}. Total kudos: ${survey.kudos}`); }); } @@ -191,10 +210,10 @@ export class NewCopilotSurveyComponent implements OnInit { const { org, repo, prNumber } = this.parseGitHubPRUrl(this.params['url']); const survey = { id: this.id, - userId: this.params['author'], + userId: this.surveyForm.value.userId, org, - repo, - prNumber, + repo: this.surveyForm.value.repo, + prNumber: this.surveyForm.value.prNumber, usedCopilot: this.surveyForm.value.usedCopilot, percentTimeSaved: Number(this.surveyForm.value.percentTimeSaved), reason: this.surveyForm.value.reason, @@ -219,4 +238,25 @@ export class NewCopilotSurveyComponent implements OnInit { formatPercent(value: number): string { return `${value}%` } + + validateUserId() { + const userIdControl = this.surveyForm.get('userId'); + if (userIdControl) { + const userId = userIdControl.value; + if (userId) { + this.membersService.getMemberByLogin(userId).subscribe( + (response) => { + if (response) { + userIdControl.setErrors(null); + } else { + userIdControl.setErrors({ invalidUserId: true }); + } + }, + () => { + userIdControl.setErrors({ invalidUserId: true }); + } + ); + } + } + } } diff --git a/frontend/src/app/services/api/copilot-survey.service.ts b/frontend/src/app/services/api/copilot-survey.service.ts index 904ce17..3588234 100644 --- a/frontend/src/app/services/api/copilot-survey.service.ts +++ b/frontend/src/app/services/api/copilot-survey.service.ts @@ -41,8 +41,7 @@ export class CopilotSurveyService { }); } - getRecentSurveysWithGoodReasons(minReasonLength: number = 20){ - //const params = new HttpParams().set('minReasonLength', minReasonLength.toString()); + getRecentSurveysWithGoodReasons(minReasonLength: number) { return this.http.get(`${this.apiUrl}/recent-good-reasons/${minReasonLength}`); } @@ -57,4 +56,11 @@ export class CopilotSurveyService { updateSurvey(survey: Survey) { return this.http.put(`${this.apiUrl}/${survey.id}`, survey); } +//create a call to the backend to update the kudos property of a survey + updateKudos(id: number) { + + return this.http.put(`${this.apiUrl}/kudos/${id}`, {}); + } + + } diff --git a/frontend/src/app/services/api/members.service.ts b/frontend/src/app/services/api/members.service.ts index 2728369..467619c 100644 --- a/frontend/src/app/services/api/members.service.ts +++ b/frontend/src/app/services/api/members.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import { serverUrl } from '../server.service'; import { HttpClient } from '@angular/common/http'; import { Endpoints } from '@octokit/types'; +import { catchError } from 'rxjs/operators'; +import { throwError } from 'rxjs'; @Injectable({ providedIn: 'root' @@ -16,4 +18,12 @@ export class MembersService { params: org ? { org } : undefined }); } + + getMemberByLogin(login: string) { + return this.http.get(`${this.apiUrl}/${login}`).pipe( + catchError((error) => { + return throwError(() => new Error('User not found')); + }) + ); + } }