From 69b55a3c3cc4b22ccbc5fbe7d8620a4bc8a82950 Mon Sep 17 00:00:00 2001 From: jonandernovella Date: Fri, 8 Sep 2023 12:08:31 +0200 Subject: [PATCH 1/4] feat: fetch activities from /projects - Frontend changes to use the updated /api/activities endpoint - Updated /api/activities to use redmine /projects instead of /enumerations This way we can get the actual activity entries that are allowed by redmine. --- backend/api/api.go | 2 +- backend/api/getActivitiesHandler.go | 57 ++++++++++++------------ backend/api/getPriorityEntriesHandler.go | 2 +- backend/api/handlers_test.go | 35 +++++++++------ backend/api/postTimeEntriesHandler.go | 2 + backend/internal/redmine/models.go | 7 +++ frontend/src/components/QuickAdd.tsx | 6 +-- frontend/src/model.tsx | 1 + 8 files changed, 65 insertions(+), 47 deletions(-) diff --git a/backend/api/api.go b/backend/api/api.go index 3744074f..f43cdff6 100644 --- a/backend/api/api.go +++ b/backend/api/api.go @@ -68,7 +68,7 @@ func Setup() *fiber.App { app.Get("/api/issues", getIssuesHandler) - app.Get("/api/activities", getActivitiesHandler) + app.Get("/api/activities", getProjectActivitiesHandler) app.Get("/api/priority_entries", getPriorityEntriesHandler) diff --git a/backend/api/getActivitiesHandler.go b/backend/api/getActivitiesHandler.go index 3e14e69d..94f5ad53 100644 --- a/backend/api/getActivitiesHandler.go +++ b/backend/api/getActivitiesHandler.go @@ -6,29 +6,26 @@ import ( "sort" "strconv" "urdr-api/internal/config" + "urdr-api/internal/redmine" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/proxy" ) -type TimeEntryActivityResponse struct { - TimeEntryActivities []struct { - Id int `json:"id"` - Name string `json:"name"` - IsDefault bool `json:"is_default"` - Active bool `json:"active"` - } `json:"time_entry_activities"` -} - // getActivitiesHandler godoc -// @Summary (Mostly) a proxy for the "/enumerations/time_entry_activities.json" Redmine endpoint +// @Summary Get a list of activities from the Redmine projects endpoint // @Accept json // @Produce json // @Failure 401 {string} error "Unauthorized" // @Failure 500 {string} error "Internal Server Error" // @Router /api/activities [get] -// @Param session_id query string false "Issue ID" default(0) -func getActivitiesHandler(c *fiber.Ctx) error { +// @Param project_id query string false "Project ID" default(0) +// @Param issue_id query string false "Issue ID" default(0) +func getProjectActivitiesHandler(c *fiber.Ctx) error { + redmineProjectId, err := strconv.Atoi(c.Query("project_id", "0")) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } redmineIssueId, err := strconv.Atoi(c.Query("issue_id", "0")) if err != nil { return c.SendStatus(fiber.StatusInternalServerError) @@ -38,8 +35,16 @@ func getActivitiesHandler(c *fiber.Ctx) error { return err } - redmineURL := fmt.Sprintf("%s/enumerations/time_entry_activities.json", - config.Config.Redmine.URL) + var redmineURL string + + // If we don't have a real project ID, return an empty list of activities. + if redmineProjectId == 0 { + emptyListResponse := redmine.ProjectEntry{} + return c.JSON(emptyListResponse) + } else { + redmineURL = fmt.Sprintf("%s/projects/%d.json?include=time_entry_activities", + config.Config.Redmine.URL, redmineProjectId) + } // Proxy the request to Redmine if err := proxy.Do(c, redmineURL); err != nil { @@ -48,7 +53,7 @@ func getActivitiesHandler(c *fiber.Ctx) error { return nil } - activitiesResponse := TimeEntryActivityResponse{} + activitiesResponse := redmine.ProjectEntry{} if err := json.Unmarshal(c.Response().Body(), &activitiesResponse); err != nil { c.Response().Reset() @@ -56,25 +61,19 @@ func getActivitiesHandler(c *fiber.Ctx) error { } // Sort the activities list alphabetically on the name. - sort.Slice(activitiesResponse.TimeEntryActivities, func(i, j int) bool { - return activitiesResponse.TimeEntryActivities[i].Name < - activitiesResponse.TimeEntryActivities[j].Name + sort.Slice(activitiesResponse.Project.TimeEntryActivities, func(i, j int) bool { + return activitiesResponse.Project.TimeEntryActivities[i].Name < + activitiesResponse.Project.TimeEntryActivities[j].Name }) - // Bypass filtering if we don't have a real issue ID. - if redmineIssueId == 0 { - // Return all activities. - return c.JSON(activitiesResponse) - } - - filteredActivities := TimeEntryActivityResponse{} + filteredActivities := redmine.ProjectEntry{} - for _, activity := range activitiesResponse.TimeEntryActivities { + for _, activity := range activitiesResponse.Project.TimeEntryActivities { if db.IsValidEntry(redmineIssueId, activity.Id) { - filteredActivities.TimeEntryActivities = - append(filteredActivities.TimeEntryActivities, activity) + filteredActivities.Project.TimeEntryActivities = + append(filteredActivities.Project.TimeEntryActivities, activity) } } - return c.JSON(filteredActivities) + return c.JSON(filteredActivities.Project) } diff --git a/backend/api/getPriorityEntriesHandler.go b/backend/api/getPriorityEntriesHandler.go index 06ed8ec6..244938a2 100644 --- a/backend/api/getPriorityEntriesHandler.go +++ b/backend/api/getPriorityEntriesHandler.go @@ -62,7 +62,7 @@ func getPriorityEntriesHandler(c *fiber.Ctx) error { // Now fetch the activities from Redmine and fill out the // activity names. c.Response().Reset() - if err := getActivitiesHandler(c); err != nil { + if err := getProjectActivitiesHandler(c); err != nil { // There was some error in the handler. return err } else if c.Response().StatusCode() != fiber.StatusOK { diff --git a/backend/api/handlers_test.go b/backend/api/handlers_test.go index 2cc5992d..a3730e21 100644 --- a/backend/api/handlers_test.go +++ b/backend/api/handlers_test.go @@ -102,18 +102,19 @@ func Test_Handlers(t *testing.T) { createdEntry, _ := json.Marshal(entryResult) fetchedEntries, _ := json.Marshal(entriesResult) - - entryActs := redmine.TimeEntryActivitiesResult{ - TimeEntryActivities: []redmine.TimeEntryActivity{ - { - Id: 1, - Name: "Test activity", - IsDefault: true, + projectEntry := redmine.ProjectEntry{ + Project: redmine.Project{ + TimeEntryActivities: []redmine.TimeEntryActivity{ + { + Id: 1, + Name: "test activity", + IsDefault: true, + }, }, }, } - entryActsResponse, _ := json.Marshal(entryActs) + projectActivities, _ := json.Marshal(projectEntry) issueAct := []api.PriorityEntry{ { @@ -179,8 +180,8 @@ func Test_Handlers(t *testing.T) { } case "/issues.json": _, err = w.Write(issuesResponse) - case "/enumerations/time_entry_activities.json": - _, err = w.Write(entryActsResponse) + case "/projects/1.json": + _, err = w.Write(projectActivities) default: log.Debugf("%s.\n", endpoint) _, err = w.Write(nil) @@ -315,7 +316,7 @@ func Test_Handlers(t *testing.T) { { name: "Entry activities", method: "GET", - endpoint: "/api/activities", + endpoint: "/api/activities?project_id=1&issue_id=1", testRedmine: fakeRedmine, useSessionHeader: true, statusCode: fiber.StatusOK, @@ -323,7 +324,7 @@ func Test_Handlers(t *testing.T) { { name: "Entry activities 401", method: "GET", - endpoint: "/api/activities", + endpoint: "/api/activities?project_id=1&issue_id=1", testRedmine: fakeRedmine, useSessionHeader: false, statusCode: fiber.StatusUnauthorized, @@ -331,11 +332,19 @@ func Test_Handlers(t *testing.T) { { name: "Entry activities 422", method: "GET", - endpoint: "/api/activities", + endpoint: "/api/activities?project_id=1&issue_id=1", testRedmine: badRedmine, useSessionHeader: true, statusCode: fiber.StatusUnprocessableEntity, }, + { + name: "Entry activities no params", + method: "GET", + endpoint: "/api/activities", + testRedmine: badRedmine, + useSessionHeader: true, + statusCode: fiber.StatusOK, + }, { name: "priority_entries POST", method: "POST", diff --git a/backend/api/postTimeEntriesHandler.go b/backend/api/postTimeEntriesHandler.go index bb2b7c2d..961ea27e 100644 --- a/backend/api/postTimeEntriesHandler.go +++ b/backend/api/postTimeEntriesHandler.go @@ -73,8 +73,10 @@ func postTimeEntriesHandler(c *fiber.Ctx) error { log.Errorf("proxy.Do() failed: %v\n", err) return c.SendStatus(fiber.StatusInternalServerError) } + log.Debugf("respose from redmine: %s", c.Response().Body()) if session, err := store.Get(c); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) } else { // Extend the session's expiry time to a week. diff --git a/backend/internal/redmine/models.go b/backend/internal/redmine/models.go index e72ceb28..c5bf7c70 100644 --- a/backend/internal/redmine/models.go +++ b/backend/internal/redmine/models.go @@ -75,3 +75,10 @@ type Group struct { Id int `json:"id"` Name string `json:"name"` } +type Project struct { + TimeEntryActivities []TimeEntryActivity `json:"time_entry_activities"` +} + +type ProjectEntry struct { + Project Project `json:"project"` +} diff --git a/frontend/src/components/QuickAdd.tsx b/frontend/src/components/QuickAdd.tsx index e0a26f65..adc16865 100644 --- a/frontend/src/components/QuickAdd.tsx +++ b/frontend/src/components/QuickAdd.tsx @@ -36,7 +36,7 @@ export const QuickAdd = ({ React.useEffect(() => { let endpoint = "/api/activities"; - if (issue) endpoint += "?issue_id=" + issue.id; + if (issue) endpoint += "?project_id=" + (issue.project.id ? issue.project.id : "0") + "&issue_id=" + (issue.id ? issue.id : "0"); let didCancel = false; const loadActivities = async () => { let result: { time_entry_activities: IdName[] } = await getApiEndpoint( @@ -44,8 +44,8 @@ export const QuickAdd = ({ context ); if (!didCancel && result) { - setActivities(result.time_entry_activities); - setActivity(activity ? activity : result.time_entry_activities[0]); + setActivities(result.time_entry_activities ? result.time_entry_activities : []); + setActivity(activity ? activity : Array.isArray(result.time_entry_activities) ? result.time_entry_activities[0] : null); } }; diff --git a/frontend/src/model.tsx b/frontend/src/model.tsx index 982c2737..990b29d8 100644 --- a/frontend/src/model.tsx +++ b/frontend/src/model.tsx @@ -24,6 +24,7 @@ export interface IssueActivityPair { export interface Issue { id: number; subject: string; + project: IdName; } export interface TimeEntry { From 45ecbff556cb7610e4818c45e9178bf3c7943fdc Mon Sep 17 00:00:00 2001 From: Jon Ander Novella Date: Mon, 11 Sep 2023 09:46:48 +0200 Subject: [PATCH 2/4] chore: Add comment about Project type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andreas Kusalananda Kähäri --- backend/internal/redmine/models.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/internal/redmine/models.go b/backend/internal/redmine/models.go index c5bf7c70..5b9dbdf1 100644 --- a/backend/internal/redmine/models.go +++ b/backend/internal/redmine/models.go @@ -75,6 +75,8 @@ type Group struct { Id int `json:"id"` Name string `json:"name"` } +// The Project type is only ever used for holding the activities that are valid for a given project. +// Apart from that, Urdr mainly deals with Issues, not Projects. type Project struct { TimeEntryActivities []TimeEntryActivity `json:"time_entry_activities"` } From 65106a95b1509b035f158490afc4775f57a44ec3 Mon Sep 17 00:00:00 2001 From: jonandernovella Date: Mon, 11 Sep 2023 09:58:23 +0200 Subject: [PATCH 3/4] fix: More robust checks --- frontend/src/components/QuickAdd.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/QuickAdd.tsx b/frontend/src/components/QuickAdd.tsx index adc16865..975c1381 100644 --- a/frontend/src/components/QuickAdd.tsx +++ b/frontend/src/components/QuickAdd.tsx @@ -45,7 +45,7 @@ export const QuickAdd = ({ ); if (!didCancel && result) { setActivities(result.time_entry_activities ? result.time_entry_activities : []); - setActivity(activity ? activity : Array.isArray(result.time_entry_activities) ? result.time_entry_activities[0] : null); + setActivity(activity ? activity : (Array.isArray(result.time_entry_activities) && result.time_entry_activities.length > 0) ? result.time_entry_activities[0] : null); } }; From a361ade0da040591df5697490902b76937a86157 Mon Sep 17 00:00:00 2001 From: jonandernovella Date: Mon, 11 Sep 2023 09:58:45 +0200 Subject: [PATCH 4/4] fix: disable runtime errors overlay --- frontend/webpack.config.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 3dfe186a..bb577725 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -43,7 +43,12 @@ module.exports = { historyApiFallback: true, static: './public', client: { - webSocketURL: 'ws://localhost:4567/ws' + webSocketURL: 'ws://localhost:4567/ws', + overlay: { + errors: true, + warnings: false, + runtimeErrors: false + } } }, plugins: [