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 + }} + + + {{/modal-dialog}} + {{/if}} + + + {{#if this.showModal2}} + {{#modal-dialog + onClose=(action (action (mut this.showModal2) false)) + targetAttachment="center" + translucentOverlay=true + }} + + + {{/modal-dialog}} + {{/if}} + {{#if this.docsWaitingForReview}}