diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 000000000..13566b81b
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/hermes.iml b/.idea/hermes.iml
new file mode 100644
index 000000000..5e764c4f0
--- /dev/null
+++ b/.idea/hermes.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 000000000..d0225c63f
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..3668dc8ca
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 000000000..1f830a643
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..35eb1ddfb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/api/products.go b/internal/api/products.go
index 3c6103ff9..41d85fd0c 100644
--- a/internal/api/products.go
+++ b/internal/api/products.go
@@ -2,58 +2,211 @@ package api
import (
"encoding/json"
+ "fmt"
+ "github.com/hashicorp-forge/hermes/internal/structs"
+ "gorm.io/gorm"
"net/http"
"github.com/hashicorp-forge/hermes/internal/config"
- "github.com/hashicorp-forge/hermes/internal/structs"
"github.com/hashicorp-forge/hermes/pkg/algolia"
+ "github.com/hashicorp-forge/hermes/pkg/models"
"github.com/hashicorp/go-hclog"
)
+type ProductRequest struct {
+ ProductName string `json:"productName,omitempty"`
+ ProductAbbreviation string `json:"productAbbreviation,omitempty"`
+}
+
// ProductsHandler returns the product mappings to the Hermes frontend.
-func ProductsHandler(cfg *config.Config, a *algolia.Client, log hclog.Logger) http.Handler {
+func ProductsHandler(cfg *config.Config, ar *algolia.Client,
+ aw *algolia.Client, db *gorm.DB, log hclog.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // Only allow GET requests.
- if r.Method != http.MethodGet {
- w.WriteHeader(http.StatusMethodNotAllowed)
- return
- }
- // Get products and associated data from Algolia
- products, err := getProductsData(a)
- if err != nil {
- log.Error("error getting products from algolia", "error", err)
- http.Error(w, "Error getting product mappings",
- http.StatusInternalServerError)
- return
- }
+ switch r.Method {
+ case "POST":
+ // Decode request.
+ var req ProductRequest
+ if err := decodeRequest(r, &req); err != nil {
+ log.Error("error decoding products request", "error", err)
+ http.Error(w, fmt.Sprintf("Bad request: %q", err),
+ http.StatusBadRequest)
+ return
+ }
+
+ // Add the data to both algolia and the Postgres Database
+ err := AddNewProducts(ar, aw, db, req)
+ if err != nil {
+ log.Error("error inserting new product/Business Unit", "error", err)
+ http.Error(w, "Error inserting products",
+ http.StatusInternalServerError)
+ return
+ }
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
+ // Send success response
+ // Send success response with success message
+ response := struct {
+ Message string `json:"message"`
+ }{
+ Message: "Product/BU Inserted successfully",
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err = enc.Encode(response)
- enc := json.NewEncoder(w)
- err = enc.Encode(products)
- if err != nil {
- log.Error("error encoding products response", "error", err)
- http.Error(w, "Error getting products",
- http.StatusInternalServerError)
+ case "GET":
+ // Get products and associated data from Algolia
+ products, err := getProductsData(db)
+ if err != nil {
+ log.Error("error getting products from database", "error", err)
+ http.Error(w, "Error getting product mappings",
+ http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(products)
+ if err != nil {
+ log.Error("error encoding products response", "error", err)
+ http.Error(w, "Error getting products",
+ http.StatusInternalServerError)
+ return
+ }
+ default:
+ w.WriteHeader(http.StatusMethodNotAllowed)
return
+
}
+
})
}
// getProducts gets the product or area name and their associated
-// data from Algolia
-func getProductsData(a *algolia.Client) (map[string]structs.ProductData, error) {
- p := structs.Products{
+// data from Database
+func getProductsData(db *gorm.DB) (map[string]struct {
+ Abbreviation string `json:"abbreviation"`
+ PerDocTypeData interface{} `json:"perDocTypeData"`
+}, error) {
+ var products []models.Product
+
+ if err := db.Select("name, abbreviation").Find(&products).Error; err != nil {
+ return nil, err
+ }
+
+ productData := make(map[string]struct {
+ Abbreviation string `json:"abbreviation"`
+ PerDocTypeData interface{} `json:"perDocTypeData"`
+ })
+
+ for _, product := range products {
+ productData[product.Name] = struct {
+ Abbreviation string `json:"abbreviation"`
+ PerDocTypeData interface{} `json:"perDocTypeData"`
+ }{
+ Abbreviation: product.Abbreviation,
+ PerDocTypeData: nil, // You can populate this field as needed
+ }
+ }
+
+ return productData, nil
+}
+
+// AddNewProducts This helper fuction add the newly added product in both algolia and upserts it
+// in the postgres Database
+func AddNewProducts(ar *algolia.Client,
+ aw *algolia.Client, db *gorm.DB, req ProductRequest) error {
+
+ // Step 1: Update the algolia object
+ var productsObj = structs.Products{
ObjectID: "products",
Data: make(map[string]structs.ProductData, 0),
}
+ // Retrieve the existing productsObj from Algolia
+ err := ar.Internal.GetObject("products", &productsObj)
+ if err != nil {
+ return fmt.Errorf("error retrieving existing products object from Algolia : %w", err)
+ }
- err := a.Internal.GetObject("products", &p)
+ // Add the new value to the productsObj
+ productsObj.Data[req.ProductName] = structs.ProductData{
+ Abbreviation: req.ProductAbbreviation,
+ }
+
+ // Save the updated productsObj back to Algolia
+ // this replaces the old object completely
+ // Save Algolia products object.
+ res, err := aw.Internal.SaveObject(&productsObj)
if err != nil {
- return nil, err
+ return fmt.Errorf("error saving Algolia products object: %w", err)
+ }
+ err = res.Wait()
+ if err != nil {
+ return fmt.Errorf("error saving Algolia products object: %w", err)
}
- return p.Data, nil
+ // Step 2: upsert in the db
+ pm := models.Product{
+ Name: req.ProductName,
+ Abbreviation: req.ProductAbbreviation,
+ }
+ if err := pm.Upsert(db); err != nil {
+ return fmt.Errorf("error upserting product: %w", err)
+ }
+
+ return nil
}
+
+// Below Code uses Algolia for fetching
+// ProductsHandler returns the product mappings to the Hermes frontend.
+//func ProductsHandler(cfg *config.Config, ar *algolia.Client,
+// aw *algolia.Client, log hclog.Logger) http.Handler {
+// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+// // Only allow GET requests.
+// if r.Method != http.MethodGet {
+// w.WriteHeader(http.StatusMethodNotAllowed)
+// return
+// }
+//
+// // Get products and associated data from Algolia
+// products, err := getProductsData(ar)
+// if err != nil {
+// log.Error("error getting products from algolia", "error", err)
+// http.Error(w, "Error getting product mappings",
+// http.StatusInternalServerError)
+// return
+// }
+//
+// w.Header().Set("Content-Type", "application/json")
+// w.WriteHeader(http.StatusOK)
+//
+// enc := json.NewEncoder(w)
+// err = enc.Encode(products)
+// if err != nil {
+// log.Error("error encoding products response", "error", err)
+// http.Error(w, "Error getting products",
+// http.StatusInternalServerError)
+// return
+// }
+// })
+//}
+//
+//// getProducts gets the product or area name and their associated
+//// data from Algolia
+//func getProductsData(ar *algolia.Client) (map[string]structs.ProductData, error) {
+// p := structs.Products{
+// ObjectID: "products",
+// Data: make(map[string]structs.ProductData, 0),
+// }
+//
+// err := ar.Internal.GetObject("products", &p)
+// if err != nil {
+// return nil, err
+// }
+//
+// return p.Data, nil
+//}
+//
diff --git a/internal/api/teams.go b/internal/api/teams.go
new file mode 100644
index 000000000..de7c2302b
--- /dev/null
+++ b/internal/api/teams.go
@@ -0,0 +1,191 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/hashicorp-forge/hermes/internal/config"
+ "github.com/hashicorp-forge/hermes/pkg/algolia"
+ "github.com/hashicorp-forge/hermes/pkg/models"
+ "github.com/hashicorp/go-hclog"
+ "gorm.io/gorm"
+ "net/http"
+)
+
+type TeamRequest struct {
+ TeamName string `json:"teamName,omitempty"`
+ TeamAbbreviation string `json:"teamAbbreviation,omitempty"`
+ TeamBU string `json:"teamBU,omitempty"`
+}
+
+// TeamsHandler returns the product mappings to the Hermes frontend.
+func TeamsHandler(cfg *config.Config, ar *algolia.Client,
+ aw *algolia.Client, db *gorm.DB, log hclog.Logger) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ switch r.Method {
+ case "POST":
+ // Decode request.
+ var req TeamRequest
+ if err := decodeRequest(r, &req); err != nil {
+ log.Error("error decoding teams request", "error", err)
+ http.Error(w, fmt.Sprintf("Bad request: %q", err),
+ http.StatusBadRequest)
+ return
+ }
+
+ // Add the data to both algolia and the Postgres Database
+ err := AddNewTeams(ar, aw, db, req)
+ if err != nil {
+ log.Error("error inserting new product/Business Unit", "error", err)
+ http.Error(w, "Error inserting products",
+ http.StatusInternalServerError)
+ return
+ }
+
+ // Send success response
+ // Send success response with success message
+ response := struct {
+ Message string `json:"message"`
+ }{
+ Message: "Team/Pod Inserted successfully",
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err = enc.Encode(response)
+
+ case "GET":
+ // Get products and associated data from Algolia
+ products, err := getTeamsData(db)
+ if err != nil {
+ log.Error("error getting products from database", "error", err)
+ http.Error(w, "Error getting product mappings",
+ http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(products)
+ if err != nil {
+ log.Error("error encoding products response", "error", err)
+ http.Error(w, "Error getting products",
+ http.StatusInternalServerError)
+ return
+ }
+ default:
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+
+ }
+
+ })
+}
+
+// getProducts gets the product or area name and their associated
+// data from Database
+func getTeamsData(db *gorm.DB) (map[string]struct {
+ Abbreviation string `json:"abbreviation"`
+ BU string `json:"BU"`
+ PerDocTypeData interface{} `json:"perDocDataType"`
+}, error) {
+ var teams []models.Team
+
+ if err := db.Find(&teams).Error; err != nil {
+ return nil, fmt.Errorf("failed to fetch teams: %w", err)
+ }
+
+ teamsData := make(map[string]struct {
+ Abbreviation string `json:"abbreviation"`
+ BU string `json:"BU"`
+ PerDocTypeData interface{} `json:"perDocDataType"`
+ })
+
+ for _, team := range teams {
+ teamsData[team.Name] = struct {
+ Abbreviation string `json:"abbreviation"`
+ BU string `json:"BU"`
+ PerDocTypeData interface{} `json:"perDocDataType"`
+ }{
+ Abbreviation: team.Abbreviation,
+ BU: team.BU.Name,
+ PerDocTypeData: nil,
+ }
+ }
+
+ return teamsData, nil
+}
+
+// AddNewTeams This helper function add the newly added product in both algolia and upserts it
+// in the postgres Database
+func AddNewTeams(ar *algolia.Client,
+ aw *algolia.Client, db *gorm.DB, req TeamRequest) error {
+
+ //// Step 1: Update the algolia object
+ //var teamsObj = structs.Teams{
+ // ObjectID: "teams",
+ // Data: make(map[string]structs.TeamData, 0),
+ //}
+ //// Retrieve the existing teamsObj from Algolia
+ //err := ar.Internal.GetObject("teams", &teamsObj)
+ //if err != nil {
+ // if algoliaErr, ok := err.(*search.); ok && algoliaErr.StatusCode == 404 {
+ // // Object does not exist, create it
+ // _, err := index.SaveObject(objectID, data)
+ // if err != nil {
+ // return fmt.Errorf("error creating object: %w", err)
+ // }
+ // } else {
+ // return fmt.Errorf("error fetching object: %w", err)
+ // }
+ //} else {
+ // // Object exists, update it
+ // _, err := index.SaveObject(objectID, data)
+ // if err != nil {
+ // return fmt.Errorf("error updating object: %w", err)
+ // }
+ //}
+ //
+ // if err == algoli search {
+ // // Object not found, it's the first run, handle accordingly
+ // // For example, initialize the teamsObj with default values
+ // teamsObj = &structs.Teams{
+ // ObjectID: "teams",
+ // Data: make(map[string]structs.Team, 0),
+ // }
+ // } else {
+ // // Other error occurred while retrieving the object
+ // return fmt.Errorf("error retrieving existing teams object from Algolia: %w", err)
+ // }
+ //}
+ //
+ //// Add the new value to the productsObj
+ //productsObj.Data[req.ProductName] = structs.ProductData{
+ // Abbreviation: req.ProductAbbreviation,
+ //}
+ //
+ //// Save the updated productsObj back to Algolia
+ //// this replaces the old object completely
+ //// Save Algolia products object.
+ //res, err := aw.Internal.SaveObject(&productsObj)
+ //if err != nil {
+ // return fmt.Errorf("error saving Algolia products object: %w", err)
+ //}
+ //err = res.Wait()
+ //if err != nil {
+ // return fmt.Errorf("error saving Algolia products object: %w", err)
+ //}
+
+ // Step 2: upsert in the db
+ pm := models.Team{
+ Name: req.TeamName,
+ Abbreviation: req.TeamAbbreviation,
+ }
+ if err := pm.Upsert(db, req.TeamBU); err != nil {
+ return fmt.Errorf("error upserting product: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/cmd/commands/server/server.go b/internal/cmd/commands/server/server.go
index a7a2c073b..c66b83a7e 100644
--- a/internal/cmd/commands/server/server.go
+++ b/internal/cmd/commands/server/server.go
@@ -367,11 +367,11 @@ func (c *Command) Run(args []string) int {
return 1
}
- // Register products.
- if err := registerProducts(cfg, algoWrite, db); err != nil {
- c.UI.Error(fmt.Sprintf("error registering products: %v", err))
- return 1
- }
+ //// Register products.
+ //if err := registerProducts(cfg, algoWrite, db); err != nil {
+ // c.UI.Error(fmt.Sprintf("error registering products: %v", err))
+ // return 1
+ //}
// Register document types.
// TODO: remove this and use the database for all document type lookups.
@@ -410,7 +410,8 @@ func (c *Command) Run(args []string) int {
{"/api/v1/me/subscriptions",
api.MeSubscriptionsHandler(cfg, c.Log, goog, db)},
{"/api/v1/people", api.PeopleDataHandler(cfg, c.Log, goog)},
- {"/api/v1/products", api.ProductsHandler(cfg, algoSearch, c.Log)},
+ {"/api/v1/products", api.ProductsHandler(cfg, algoSearch, algoWrite, db, c.Log)},
+ {"/api/v1/teams", api.TeamsHandler(cfg, algoSearch, algoWrite, db, c.Log)},
{"/api/v1/reviews/",
api.ReviewHandler(cfg, c.Log, algoSearch, algoWrite, goog, db)},
{"/api/v1/web/analytics", api.AnalyticsHandler(c.Log)},
diff --git a/internal/structs/teams.go b/internal/structs/teams.go
new file mode 100644
index 000000000..f42a8574a
--- /dev/null
+++ b/internal/structs/teams.go
@@ -0,0 +1,24 @@
+package structs
+
+// TeamDocTypeData contains data for each document type.
+type TeamDocTypeData struct {
+ FolderID string `json:"folderID"`
+ LatestDocNumber int `json:"latestDocNumber"`
+}
+
+// TeamData is the data associated with a product or area.
+// This may include product abbreviation, etc.
+type TeamData struct {
+ Abbreviation string `json:"abbreviation"`
+ BUID uint `json:"buid"`
+ // PerDocTypeData is a map of each document type (RFC, PRD, etc)
+ // to the associated data
+ PerDocTypeData map[string]TeamDocTypeData `json:"perDocTypeData"`
+}
+
+// Teams is the slice of product data.
+type Teams struct {
+ // ObjectID should be "products"
+ ObjectID string `json:"objectID,omitempty"`
+ Data map[string]TeamData `json:"data"`
+}
diff --git a/pkg/models/product.go b/pkg/models/product.go
index 97e308fa5..4d7a5e9a6 100644
--- a/pkg/models/product.go
+++ b/pkg/models/product.go
@@ -19,6 +19,9 @@ type Product struct {
// UserSubscribers are the users that subscribed to this product.
UserSubscribers []User `gorm:"many2many:user_product_subscriptions;"`
+
+ // Teams is the list of teams associated with the BU.
+ Teams []Team `gorm:"foreignKey:BUID;constraint:Teams_BU_mapping"`
}
// FirstOrCreate finds the first product by name or creates a record if it does
@@ -68,11 +71,43 @@ func (p *Product) Get(db *gorm.DB) error {
Error
}
-// Upsert updates or inserts a product into database db.
+//// Upsert updates or inserts a product into database db.
+//func (p *Product) Upsert(db *gorm.DB) error {
+// return db.
+// Where(Product{Name: p.Name}).
+// Assign(*p).
+// FirstOrCreate(&p).
+// Error
+//}
+
+// Upsert updates or inserts a BU into the database, including associated teams.
func (p *Product) Upsert(db *gorm.DB) error {
- return db.
- Where(Product{Name: p.Name}).
- Assign(*p).
- FirstOrCreate(&p).
- Error
+ return db.Transaction(func(tx *gorm.DB) error {
+ // Upsert the BU.
+ if err := tx.
+ Omit(clause.Associations).
+ Assign(*p).
+ Clauses(clause.OnConflict{DoNothing: true}).
+ FirstOrCreate(&p).
+ Error; err != nil {
+ return err
+ }
+
+ // Save the associated teams.
+ for _, team := range p.Teams {
+ // Assign the BU ID to the team.
+ team.BUID = p.ID
+
+ // Upsert the team.
+ if err := tx.
+ Omit(clause.Associations).
+ Clauses(clause.OnConflict{DoNothing: true}).
+ Create(&team).
+ Error; err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
}
diff --git a/pkg/models/team.go b/pkg/models/team.go
new file mode 100644
index 000000000..8d407428f
--- /dev/null
+++ b/pkg/models/team.go
@@ -0,0 +1,84 @@
+package models
+
+import (
+ "errors"
+ "fmt"
+ validation "github.com/go-ozzo/ozzo-validation/v4"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+type Team struct {
+ gorm.Model
+
+ // Name is the name of the team
+ Name string `gorm:"default:null;index;not null;type:citext;unique"`
+
+ // Abbreviation is a short group of capitalized letters to represent the team.
+ Abbreviation string `gorm:"default:null;not null;type:citext;unique"`
+
+ // BUName is the business unit that this team belongs to
+ BUID uint `gorm:"default:null;not null;type:citext;"`
+
+ // UserSubscribers are the users that subscribed to this product.
+ BU Product
+}
+
+// Upsert upserts a team along with its associated BU into the database.
+// If a BU with the given name already exists, it is used; otherwise, an error is returned.
+func (t *Team) Upsert(db *gorm.DB, prdName string) error {
+ return db.Transaction(func(tx *gorm.DB) error {
+ // Check if the BU with the given name already exists.
+ var existingPrd Product
+ if err := tx.Where("name = ?", prdName).First(&existingPrd).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return fmt.Errorf("BU '%s' does not exist", prdName)
+ }
+ return fmt.Errorf("error querying BU: %w", err)
+ }
+
+ // Set the BUID of the team to the ID of the BU.
+ t.BUID = existingPrd.ID
+ t.BU = existingPrd
+
+ // Upsert the team.
+ if err := tx.Omit(clause.Associations).
+ Assign(*t).
+ Clauses(clause.OnConflict{DoNothing: true}).
+ FirstOrCreate(&t).Error; err != nil {
+ return fmt.Errorf("error upserting team: %w", err)
+ }
+
+ // Update the BU.
+ existingPrd.Teams = append(existingPrd.Teams, *t)
+ if err := tx.Save(&existingPrd).Error; err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+// Get gets a team from database db by name, and assigns it back to the
+// receiver.
+func (t *Team) Get(db *gorm.DB) error {
+ if err := validation.ValidateStruct(t,
+ validation.Field(
+ &t.ID,
+ validation.When(t.Name == "",
+ validation.Required.Error("either ID or Name is required")),
+ ),
+ validation.Field(
+ &t.Name,
+ validation.When(t.ID == 0,
+ validation.Required.Error("either ID or Name is required"))),
+ ); err != nil {
+ return err
+ }
+
+ return db.
+ Where(Team{Name: t.Name}).
+ Preload(clause.Associations).
+ First(&t).
+ Error
+}
diff --git a/pkg/models/user.go b/pkg/models/user.go
index d5ab6f9f0..9b0b8e3da 100644
--- a/pkg/models/user.go
+++ b/pkg/models/user.go
@@ -18,6 +18,8 @@ type User struct {
// ProductSubscriptions are the products that have been subscribed to by the
// user.
+ //By default, GORM will create a join table named user_product_subscriptions to represent this association.
+ // The join table will have foreign keys that reference the primary keys of the User and Product tables.
ProductSubscriptions []Product `gorm:"many2many:user_product_subscriptions;"`
// RecentlyViewedDocs are the documents recently viewed by the user.
diff --git a/web/app/components/header/index.hbs b/web/app/components/header/index.hbs
index 8197fecf8..e7be8e9f2 100644
--- a/web/app/components/header/index.hbs
+++ b/web/app/components/header/index.hbs
@@ -1,5 +1,5 @@
-
-
{{#if this.userMenuHighlightIsShown}}
diff --git a/web/app/components/inputs/team-select/index.ts b/web/app/components/inputs/team-select/index.ts
index 64adac30d..71aa5686e 100644
--- a/web/app/components/inputs/team-select/index.ts
+++ b/web/app/components/inputs/team-select/index.ts
@@ -83,48 +83,47 @@ export default class InputsTeamSelectComponent extends Component
resp?.json());
+ this.teams = teams;
+ console.log(this.teams);
} catch (err) {
console.error(err);
throw err;
diff --git a/web/app/controllers/authenticated/dashboard.js b/web/app/controllers/authenticated/dashboard.js
index b06eaa1cb..064eee2e5 100644
--- a/web/app/controllers/authenticated/dashboard.js
+++ b/web/app/controllers/authenticated/dashboard.js
@@ -1,6 +1,18 @@
import Controller from "@ember/controller";
import { alias } from "@ember/object/computed";
import { inject as service } from "@ember/service";
+import {tracked} from "@glimmer/tracking";
+import {action} from "@ember/object";
+import FlashService from "ember-cli-flash/services/flash-messages";
+import {task, timeout} from "ember-concurrency";
+import cleanString from "../../utils/clean-string";
+import Ember from "ember";
+import {TaskForAsyncTaskFunction} from "ember-concurrency";
+import {assert} from "@ember/debug";
+import FetchService from "../../services/fetch";
+
+const AWAIT_DOC_DELAY = Ember.testing ? 0 : 1000;
+const AWAIT_DOC_CREATED_MODAL_DELAY = Ember.testing ? 0 : 1500;
export default class AuthenticatedDashboardController extends Controller {
@alias("model.docsWaitingForReview") docsWaitingForReview;
@@ -9,7 +21,172 @@ export default class AuthenticatedDashboardController extends Controller {
@service authenticatedUser;
@service("config") configSvc;
@service("recently-viewed-docs") recentDocs;
+ @service('flash-messages') flashMessages;
+ @service("fetch") fetchSvc: FetchService;
queryParams = ["latestUpdates"];
latestUpdates = "newDocs";
+ @tracked showModal1 = false;
+ @tracked businessUnitName: string = '';
+ @tracked bu_abbreviation: string = '';
+ @tracked BUIsBeingCreated = false;
+ @tracked _form: HTMLFormElement | null = null;
+
+ @tracked showModal2 = false;
+ @tracked TeamName: string = "";
+ @tracked TeamAbbreviation: string= "";
+ @tracked TeamIsBeingCreated = false;
+ @tracked TeamBU: string="";
+
+ @action
+ toggleModal1() {
+ this.toggleProperty('showModal1');
+ }
+
+ @action
+ toggleModal2() {
+ this.toggleProperty('showModal2');
+ }
+
+ /**
+ * Creates a Team, then redirects to the dashboard.
+ * On error, show a flashMessage and allow users to try again.
+ */
+ private createTeam: TaskForAsyncTaskFunction Promise> = task(async () => {
+ this.TeamIsBeingCreated = true;
+ try {
+ const bu = await this.fetchSvc
+ .fetch("/api/v1/teams", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ teamName: this.TeamName,
+ teamAbbreviation: this.TeamAbbreviation,
+ teamBU: this.TeamBU,
+ }),
+ })
+ .then((response) => response?.json());
+
+ // Wait for document to be available.
+ await timeout(AWAIT_DOC_DELAY);
+
+ this.router.transitionTo("authenticated.dashboard");
+ this.toggleModal2();
+ this.flashMessages.add({
+ title: "Success",
+ message: `New Team has been created Succesfully`,
+ type: "success",
+ timeout: 6000,
+ extendedTimeout: 1000,
+ });
+ } catch (err) {
+ this.docIsBeingCreated = false;
+ this.flashMessages.add({
+ title: "Error creating new Team",
+ message: `${err}`,
+ type: "critical",
+ timeout: 6000,
+ extendedTimeout: 1000,
+ }); }
+ finally {
+ // Hide spinning wheel or loading state
+ this.set('TeamIsBeingCreated', false);
+ }
+ });
+
+ @action submitFormteam(event: SubmitEvent) {
+ // Show spinning wheel or loading state
+ this.set('TeamIsBeingCreated', true);
+ event.preventDefault();
+
+ const formElement = event.target;
+ const formData = new FormData(formElement);
+ const formObject = Object.fromEntries(formData.entries());
+
+ // Do something with the formObject
+ console.log(formObject);
+
+ // Do something with the form values
+ this.TeamName = formObject['team-name'];
+ this.TeamAbbreviation = formObject['team-abbr'];
+ this.TeamBU = formObject['bu-name'];
+
+ // now post this info
+ this.createTeam.perform();
+
+ // Clear the form fields
+ this.TeamName = "";
+ this.TeamAbbreviation = "";
+ this.TeamBU = "";
+ }
+
+
+ /**
+ * Creates a BU, then redirects to the dashboard.
+ * On error, show a flashMessage and allow users to try again.
+ */
+ private createBU: TaskForAsyncTaskFunction Promise> = task(async () => {
+ this.BUIsBeingCreated = true;
+ try {
+ const bu = await this.fetchSvc
+ .fetch("/api/v1/products", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ productName: this.businessUnitName,
+ productAbbreviation: this.bu_abbreviation,
+ }),
+ })
+ .then((response) => response?.json());
+
+ // Wait for document to be available.
+ await timeout(AWAIT_DOC_DELAY);
+
+ this.router.transitionTo("authenticated.dashboard");
+ this.toggleModal1();
+ this.flashMessages.add({
+ title: "Success",
+ message: `New Business Unit (BU) created succesfully`,
+ type: "success",
+ timeout: 6000,
+ extendedTimeout: 1000,
+ });
+ } catch (err) {
+ this.docIsBeingCreated = false;
+ this.flashMessages.add({
+ title: "Error creating new Business Unit",
+ message: `${err}`,
+ type: "critical",
+ timeout: 6000,
+ extendedTimeout: 1000,
+ }); }
+ finally {
+ // Hide spinning wheel or loading state
+ this.set('BUIsBeingCreated', false);
+ }
+ });
+
+ @action submitFormBU(event: SubmitEvent) {
+ // Show spinning wheel or loading state
+ this.set('BUIsBeingCreated', true);
+ event.preventDefault();
+
+ const formElement = event.target;
+ const formData = new FormData(formElement);
+ const formObject = Object.fromEntries(formData.entries());
+
+ // Do something with the formObject
+ console.log(formObject);
+
+ // Do something with the form values
+ this.businessUnitName = formObject['bu-name'];
+ this.bu_abbreviation = formObject['bu-abbr'];
+
+ // now post this info
+ this.createBU.perform();
+
+ // Clear the form fields
+ this.businessUnitName = "";
+ this.bu_abbreviation = "";
+ }
}
diff --git a/web/app/routes/authenticated/dashboard.js b/web/app/routes/authenticated/dashboard.js
index 31738b025..cb447373b 100644
--- a/web/app/routes/authenticated/dashboard.js
+++ b/web/app/routes/authenticated/dashboard.js
@@ -2,6 +2,7 @@ import Route from "@ember/routing/route";
import RSVP from "rsvp";
import { inject as service } from "@ember/service";
import timeAgo from "hermes/utils/time-ago";
+import {action} from "@ember/object";
export default class DashboardRoute extends Route {
@service algolia;
@@ -103,4 +104,5 @@ export default class DashboardRoute extends Route {
}
return parentsQuery;
}
+
}
diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss
index 4679b5a80..20ce39e97 100644
--- a/web/app/styles/app.scss
+++ b/web/app/styles/app.scss
@@ -1,5 +1,4 @@
@use "./typography";
-
@use "components/action";
@use "components/toolbar";
@use "components/tooltip";
@@ -44,6 +43,13 @@
@use "tailwindcss/base";
@use "tailwindcss/components";
@use "tailwindcss/utilities";
+@import "ember-modal-dialog/ember-modal-structure";
+@import "ember-modal-dialog/ember-modal-appearance";
+
+.ember-modal-dialog{
+ width: 400px;
+}
+
*,
*::before,
diff --git a/web/app/templates/authenticated/dashboard.hbs b/web/app/templates/authenticated/dashboard.hbs
index 3445fabd4..ed63c8697 100644
--- a/web/app/templates/authenticated/dashboard.hbs
+++ b/web/app/templates/authenticated/dashboard.hbs
@@ -1,14 +1,182 @@
{{page-title "Dashboard"}}
-
+
+
+
+
+
+
+
Welcome back, {{this.authenticatedUser.info.given_name}}
Here’s all the latest updates across the organization.
+ {{#if this.showModal1}}
+ {{#modal-dialog
+ onClose=(action (action (mut this.showModal1) false))
+ targetAttachment="center"
+ translucentOverlay=true
+ }}
+
+
+
Create New Business Unit
+
+
+
+
+ {{/modal-dialog}}
+ {{/if}}
+
+
+ {{#if this.showModal2}}
+ {{#modal-dialog
+ onClose=(action (action (mut this.showModal2) false))
+ targetAttachment="center"
+ translucentOverlay=true
+ }}
+
+
+
Create New Team/Pod
+
+
+
+
+ {{/modal-dialog}}
+ {{/if}}
+
{{#if this.docsWaitingForReview}}