You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-08-08 22:36:41 +02:00
completed project section
This commit is contained in:
@ -2,20 +2,391 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/datatable"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/project"
|
||||||
|
"github.com/gorilla/schema"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User represents the User API method handler set.
|
// Projects represents the Projects API method handler set.
|
||||||
type Projects struct {
|
type Projects struct {
|
||||||
MasterDB *sqlx.DB
|
MasterDB *sqlx.DB
|
||||||
|
Redis *redis.Client
|
||||||
Renderer web.Renderer
|
Renderer web.Renderer
|
||||||
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns all the existing users in the system.
|
func urlProjectsIndex() string {
|
||||||
func (p *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
return fmt.Sprintf("/projects")
|
||||||
return p.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-index.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
|
}
|
||||||
|
|
||||||
|
func urlProjectsCreate() string {
|
||||||
|
return fmt.Sprintf("/projects/create")
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlProjectsView(projectID string) string {
|
||||||
|
return fmt.Sprintf("/projects/%s", projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlProjectsUpdate(projectID string) string {
|
||||||
|
return fmt.Sprintf("/projects/%s/update", projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index handles listing all the projects for the current account.
|
||||||
|
func (h *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
claims, err := auth.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusValues []interface{}
|
||||||
|
for _, v := range project.ProjectStatus_Values {
|
||||||
|
statusValues = append(statusValues, string(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
statusOpts := web.NewEnumResponse(ctx, nil, statusValues...)
|
||||||
|
|
||||||
|
statusFilterItems := []datatable.FilterOptionItem{}
|
||||||
|
for _, opt := range statusOpts.Options {
|
||||||
|
statusFilterItems = append(statusFilterItems, datatable.FilterOptionItem{
|
||||||
|
Display: opt.Title,
|
||||||
|
Value: opt.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []datatable.DisplayField{
|
||||||
|
datatable.DisplayField{Field: "id", Title: "ID", Visible: false, Searchable: true, Orderable: true, Filterable: false},
|
||||||
|
datatable.DisplayField{Field: "name", Title: "Project", Visible: true, Searchable: true, Orderable: true, Filterable: true, FilterPlaceholder: "filter Name"},
|
||||||
|
datatable.DisplayField{Field: "status", Title: "Status", Visible: true, Searchable: true, Orderable: true, Filterable: true, FilterPlaceholder: "All Statuses", FilterItems: statusFilterItems},
|
||||||
|
datatable.DisplayField{Field: "updated_at", Title: "Last Updated", Visible: true, Searchable: true, Orderable: true, Filterable: false},
|
||||||
|
datatable.DisplayField{Field: "created_at", Title: "Created", Visible: true, Searchable: true, Orderable: true, Filterable: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
mapFunc := func(q *project.Project, cols []datatable.DisplayField) (resp []datatable.ColumnValue, err error) {
|
||||||
|
for i := 0; i < len(cols); i++ {
|
||||||
|
col := cols[i]
|
||||||
|
var v datatable.ColumnValue
|
||||||
|
switch col.Field {
|
||||||
|
case "id":
|
||||||
|
v.Value = fmt.Sprintf("%d", q.ID)
|
||||||
|
case "name":
|
||||||
|
v.Value = q.Name
|
||||||
|
v.Formatted = fmt.Sprintf("<a href='%s'>%s</a>", urlProjectsView(q.ID), v.Value)
|
||||||
|
case "status":
|
||||||
|
v.Value = q.Status.String()
|
||||||
|
|
||||||
|
var subStatusClass string
|
||||||
|
var subStatusIcon string
|
||||||
|
switch q.Status {
|
||||||
|
case project.ProjectStatus_Active:
|
||||||
|
subStatusClass = "text-green"
|
||||||
|
subStatusIcon = "far fa-dot-circle"
|
||||||
|
case project.ProjectStatus_Disabled:
|
||||||
|
subStatusClass = "text-orange"
|
||||||
|
subStatusIcon = "far fa-circle"
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Formatted = fmt.Sprintf("<span class='cell-font-status %s'><i class='%s'></i>%s</span>", subStatusClass, subStatusIcon, web.EnumValueTitle(v.Value))
|
||||||
|
case "created_at":
|
||||||
|
dt := web.NewTimeResponse(ctx, q.CreatedAt)
|
||||||
|
v.Value = dt.Local
|
||||||
|
v.Formatted = fmt.Sprintf("<span class='cell-font-date'>%s</span>", v.Value)
|
||||||
|
case "updated_at":
|
||||||
|
dt := web.NewTimeResponse(ctx, q.UpdatedAt)
|
||||||
|
v.Value = dt.Local
|
||||||
|
v.Formatted = fmt.Sprintf("<span class='cell-font-date'>%s</span>", v.Value)
|
||||||
|
default:
|
||||||
|
return resp, errors.Errorf("Failed to map value for %s.", col.Field)
|
||||||
|
}
|
||||||
|
resp = append(resp, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFunc := func(ctx context.Context, sorting string, fields []datatable.DisplayField) (resp [][]datatable.ColumnValue, err error) {
|
||||||
|
whereFilter := "account_id = ?"
|
||||||
|
res, err := project.Find(ctx, claims, h.MasterDB, project.ProjectFindRequest{
|
||||||
|
Where: &whereFilter,
|
||||||
|
Args: []interface{}{claims.Audience},
|
||||||
|
Order: strings.Split(sorting, ","),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range res {
|
||||||
|
l, err := mapFunc(a, fields)
|
||||||
|
if err != nil {
|
||||||
|
return resp, errors.Wrapf(err, "Failed to map project for display.")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = append(resp, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, err := datatable.New(ctx, w, r, h.Redis, fields, loadFunc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dt.HasCache() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := dt.Render(); ok {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"datatable": dt.Response(),
|
||||||
|
"urlProjectsCreate": urlProjectsCreate(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-index.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create handles creating a new project for the account.
|
||||||
|
func (h *Projects) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
ctxValues, err := webcontext.ContextValues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := auth.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
req := new(project.ProjectCreateRequest)
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
f := func() (bool, error) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
decoder.IgnoreUnknownKeys(true)
|
||||||
|
|
||||||
|
if err := decoder.Decode(req, r.PostForm); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
req.AccountID = claims.Audience
|
||||||
|
|
||||||
|
usr, err := project.Create(ctx, claims, h.MasterDB, *req, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch errors.Cause(err) {
|
||||||
|
default:
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||||
|
data["validationErrors"] = verr.(*weberror.Error)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a success message to the project.
|
||||||
|
webcontext.SessionFlashSuccess(ctx,
|
||||||
|
"Project Created",
|
||||||
|
"Project successfully created.")
|
||||||
|
err = webcontext.ContextSession(ctx).Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, urlProjectsView(usr.ID), http.StatusFound)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||||
|
} else if end {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data["form"] = req
|
||||||
|
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(project.ProjectCreateRequest{})); ok {
|
||||||
|
data["validationDefaults"] = verr.(*weberror.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-create.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// View handles displaying a project.
|
||||||
|
func (h *Projects) View(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
projectID := params["project_id"]
|
||||||
|
|
||||||
|
ctxValues, err := webcontext.ContextValues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := auth.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
f := func() (bool, error) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.PostForm.Get("action") {
|
||||||
|
case "archive":
|
||||||
|
err = project.Archive(ctx, claims, h.MasterDB, project.ProjectArchiveRequest{
|
||||||
|
ID: projectID,
|
||||||
|
}, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
webcontext.SessionFlashSuccess(ctx,
|
||||||
|
"Project Archive",
|
||||||
|
"Project successfully archive.")
|
||||||
|
err = webcontext.ContextSession(ctx).Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, urlProjectsIndex(), http.StatusFound)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||||
|
} else if end {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prj, err := project.ReadByID(ctx, claims, h.MasterDB, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data["project"] = prj.Response(ctx)
|
||||||
|
|
||||||
|
data["urlProjectsUpdate"] = urlProjectsUpdate(projectID)
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-view.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handles updating a project for the account.
|
||||||
|
func (h *Projects) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
projectID := params["project_id"]
|
||||||
|
|
||||||
|
ctxValues, err := webcontext.ContextValues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := auth.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
req := new(project.ProjectUpdateRequest)
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
f := func() (bool, error) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
decoder.IgnoreUnknownKeys(true)
|
||||||
|
|
||||||
|
if err := decoder.Decode(req, r.PostForm); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
req.ID = projectID
|
||||||
|
|
||||||
|
err = project.Update(ctx, claims, h.MasterDB, *req, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch errors.Cause(err) {
|
||||||
|
default:
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||||
|
data["validationErrors"] = verr.(*weberror.Error)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a success message to the project.
|
||||||
|
webcontext.SessionFlashSuccess(ctx,
|
||||||
|
"Project Updated",
|
||||||
|
"Project successfully updated.")
|
||||||
|
err = webcontext.ContextSession(ctx).Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, urlProjectsView(req.ID), http.StatusFound)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||||
|
} else if end {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prj, err := project.ReadByID(ctx, claims, h.MasterDB, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data["project"] = prj.Response(ctx)
|
||||||
|
|
||||||
|
if req.ID == "" {
|
||||||
|
req.Name = &prj.Name
|
||||||
|
req.Status = &prj.Status
|
||||||
|
}
|
||||||
|
data["form"] = req
|
||||||
|
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(project.ProjectUpdateRequest{})); ok {
|
||||||
|
data["validationDefaults"] = verr.(*weberror.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-update.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,15 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
|||||||
// Register project management pages.
|
// Register project management pages.
|
||||||
p := Projects{
|
p := Projects{
|
||||||
MasterDB: masterDB,
|
MasterDB: masterDB,
|
||||||
|
Redis: redis,
|
||||||
Renderer: renderer,
|
Renderer: renderer,
|
||||||
}
|
}
|
||||||
|
app.Handle("POST", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("GET", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("POST", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("GET", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||||
|
app.Handle("POST", "/projects/create", p.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("GET", "/projects/create", p.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||||
|
|
||||||
// Register user management pages.
|
// Register user management pages.
|
||||||
@ -63,7 +70,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
|||||||
app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||||
app.Handle("POST", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("POST", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("GET", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||||
|
|
||||||
// Register user management and authentication endpoints.
|
// Register user management and authentication endpoints.
|
||||||
u := User{
|
u := User{
|
||||||
|
27
cmd/web-app/templates/content/projects-create.gohtml
Normal file
27
cmd/web-app/templates/content/projects-create.gohtml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{{define "title"}}Create Project{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<form class="user" method="post" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputName">Name</label>
|
||||||
|
<input type="text" class="form-control {{ ValidationFieldClass $.validationErrors "Name" }}"
|
||||||
|
placeholder="enter name" name="Name" id="inputName" value="{{ .form.Name }}" required>
|
||||||
|
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Name" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="spacer-30"></div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<input id="btnSubmit" type="submit" name="action" value="Save" class="btn btn-primary"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
{{define "js"}}
|
||||||
|
|
||||||
|
{{end}}
|
30
cmd/web-app/templates/content/projects-index.gohtml
Normal file
30
cmd/web-app/templates/content/projects-index.gohtml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{{define "title"}}Projects{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
{{ if HasRole $._Ctx "admin" }}
|
||||||
|
<a href="{{ .urlProjectsCreate }}">Create Project</a>
|
||||||
|
{{ end }}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<form method="post">
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-responsive dataTable_card">
|
||||||
|
{{ template "partials/datatable/html" . }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
{{ template "partials/datatable/style" . }}
|
||||||
|
{{ end }}
|
||||||
|
{{define "js"}}
|
||||||
|
{{ template "partials/datatable/js" . }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
//$("#dataTable_filter").hide();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{end}}
|
37
cmd/web-app/templates/content/projects-update.gohtml
Normal file
37
cmd/web-app/templates/content/projects-update.gohtml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{{define "title"}}Update Project - {{ .project.Name }}{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<form class="user" method="post" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputName">Name</label>
|
||||||
|
<input type="text" class="form-control {{ ValidationFieldClass $.validationErrors "Name" }}"
|
||||||
|
placeholder="enter name" name="Name" id="inputName" value="{{ .form.Name }}" required>
|
||||||
|
{{template "invalid-feedback" dict "validationDefaults" $.userValidationDefaults "validationErrors" $.validationErrors "fieldName" "Name" }}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="selectStatus">Status</label>
|
||||||
|
<select class="form-control {{ ValidationFieldClass $.validationErrors "Status" }}"
|
||||||
|
id="selectStatus" name="Status">
|
||||||
|
{{ range $idx, $t := .project.Status.Options }}
|
||||||
|
<option value="{{ $t.Value }}" {{ if $t.Selected }}selected="selected"{{ end }}>{{ $t.Title }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Status" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="spacer-30"></div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<input id="btnSubmit" type="submit" name="action" value="Save" class="btn btn-primary"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
{{define "js"}}
|
||||||
|
|
||||||
|
{{end}}
|
57
cmd/web-app/templates/content/projects-view.gohtml
Normal file
57
cmd/web-app/templates/content/projects-view.gohtml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{{define "title"}}Project - {{ .project.Name }}{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h4>Name</h4>
|
||||||
|
<p class="font-14">
|
||||||
|
{{ .project.Name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="{{ .urlProjectsUpdate }}" class="btn btn-outline-success"><i class="fal fa-edit"></i>Edit Details</a>
|
||||||
|
{{ if HasRole $._Ctx "admin" }}
|
||||||
|
<form method="post"><input type="hidden" name="action" value="archive" /><input type="submit" value="Archive"></form>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer-30"></div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p>
|
||||||
|
<small>Name</small><br/>
|
||||||
|
<b>{{ .project.Name }}</b>
|
||||||
|
</p>
|
||||||
|
<div class="spacer-15"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p>
|
||||||
|
<small>Status</small><br/>
|
||||||
|
{{ if .project }}
|
||||||
|
<b>
|
||||||
|
{{ if eq .project.Status.Value "active" }}
|
||||||
|
<span class="text-green"><i class="fas fa-circle"></i>{{ .project.Status.Title }}</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="text-orange"><i class="far fa-circle"></i>{{.project.Status.Title }}</span>
|
||||||
|
{{end}}
|
||||||
|
</b>
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<small>ID</small><br/>
|
||||||
|
<b>{{ .project.ID }}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{define "js"}}
|
||||||
|
|
||||||
|
{{end}}
|
@ -1,6 +1,8 @@
|
|||||||
{{define "title"}}Users{{end}}
|
{{define "title"}}Users{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
{{ if HasRole $._Ctx "admin" }}
|
||||||
<a href="{{ .urlUsersCreate }}">Create User</a>
|
<a href="{{ .urlUsersCreate }}">Create User</a>
|
||||||
|
{{ end }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{{define "title"}}Update Profile{{end}}
|
{{define "title"}}Update User - {{ .user.Name }}{{end}}
|
||||||
{{define "style"}}
|
{{define "style"}}
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{{define "title"}}Profile{{end}}
|
{{define "title"}}User - {{ .user.Name }}{{end}}
|
||||||
{{define "style"}}
|
{{define "style"}}
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -16,10 +16,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer-10"></div>
|
|
||||||
<p class="font-10"><a href="https://gravatar.com" target="_blank">Update Avatar</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
|
{{ if HasRole $._Ctx "admin" }}
|
||||||
<a href="{{ .urlUsersUpdate }}" class="btn btn-outline-success"><i class="fal fa-edit"></i>Edit Details</a>
|
<a href="{{ .urlUsersUpdate }}" class="btn btn-outline-success"><i class="fal fa-edit"></i>Edit Details</a>
|
||||||
{{ $ctxUser := ContextUser $._Ctx }}
|
{{ $ctxUser := ContextUser $._Ctx }}
|
||||||
{{ if $ctxUser }}
|
{{ if $ctxUser }}
|
||||||
@ -27,6 +26,7 @@
|
|||||||
<form method="post"><input type="hidden" name="action" value="archive" /><input type="submit" value="Archive"></form>
|
<form method="post"><input type="hidden" name="action" value="archive" /><input type="submit" value="Archive"></form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -38,8 +38,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<div id="navSectionProjects" class="collapse" data-parent="#accordionSidebar">
|
<div id="navSectionProjects" class="collapse" data-parent="#accordionSidebar">
|
||||||
<div class="bg-white py-2 collapse-inner rounded">
|
<div class="bg-white py-2 collapse-inner rounded">
|
||||||
<a class="collapse-item" href="buttons.html">Buttons</a>
|
<a class="collapse-item" href="/projects">Projects</a>
|
||||||
<a class="collapse-item" href="cards.html">Cards</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
|
|
||||||
<!-- Nav Item - Search Dropdown (Visible Only XS) -->
|
<!-- Nav Item - Search Dropdown (Visible Only XS) -->
|
||||||
<li class="nav-item dropdown no-arrow d-sm-none">
|
<!-- li class="nav-item dropdown no-arrow d-sm-none">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<i class="fas fa-search fa-fw"></i>
|
<i class="fas fa-search fa-fw"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -42,10 +42,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li -->
|
||||||
|
|
||||||
<!-- Nav Item - Alerts -->
|
<!-- Nav Item - Alerts -->
|
||||||
<li class="nav-item dropdown no-arrow mx-1">
|
<!-- li class="nav-item dropdown no-arrow mx-1">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<i class="fas fa-bell fa-fw"></i>
|
<i class="fas fa-bell fa-fw"></i>
|
||||||
< !-- Counter - Alerts -- >
|
< !-- Counter - Alerts -- >
|
||||||
@ -91,10 +91,10 @@
|
|||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
|
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li -->
|
||||||
|
|
||||||
<!-- Nav Item - Messages -->
|
<!-- Nav Item - Messages -->
|
||||||
<li class="nav-item dropdown no-arrow mx-1">
|
<!-- li class="nav-item dropdown no-arrow mx-1">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<i class="fas fa-envelope fa-fw"></i>
|
<i class="fas fa-envelope fa-fw"></i>
|
||||||
< !-- Counter - Messages -- >
|
< !-- Counter - Messages -- >
|
||||||
@ -147,7 +147,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
|
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li -->
|
||||||
|
|
||||||
<div class="topbar-divider d-none d-sm-block"></div>
|
<div class="topbar-divider d-none d-sm-block"></div>
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ func (m *AccountPreference) Response(ctx context.Context) *AccountPreferenceResp
|
|||||||
|
|
||||||
r := &AccountPreferenceResponse{
|
r := &AccountPreferenceResponse{
|
||||||
AccountID: m.AccountID,
|
AccountID: m.AccountID,
|
||||||
Name: web.NewEnumResponse(ctx, m.Name, AccountPreferenceName_Values),
|
Name: web.NewEnumResponse(ctx, m.Name, AccountPreferenceName_ValuesInterface()...),
|
||||||
Value: m.Value,
|
Value: m.Value,
|
||||||
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
@ -122,6 +122,15 @@ var AccountPreferenceName_Values = []AccountPreferenceName{
|
|||||||
AccountPreference_Time_Format,
|
AccountPreference_Time_Format,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AccountPreferenceName_ValuesInterface returns the AccountPreferenceName options as a slice interface.
|
||||||
|
func AccountPreferenceName_ValuesInterface() []interface{} {
|
||||||
|
var l []interface{}
|
||||||
|
for _, v := range AccountPreferenceName_Values {
|
||||||
|
l = append(l, v.String())
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// Scan supports reading the AccountPreferenceName value from the database.
|
// Scan supports reading the AccountPreferenceName value from the database.
|
||||||
func (s *AccountPreferenceName) Scan(value interface{}) error {
|
func (s *AccountPreferenceName) Scan(value interface{}) error {
|
||||||
asBytes, ok := value.(string)
|
asBytes, ok := value.(string)
|
||||||
|
@ -68,7 +68,7 @@ func (m *Account) Response(ctx context.Context) *AccountResponse {
|
|||||||
Country: m.Country,
|
Country: m.Country,
|
||||||
Zipcode: m.Zipcode,
|
Zipcode: m.Zipcode,
|
||||||
Timezone: m.Timezone,
|
Timezone: m.Timezone,
|
||||||
Status: web.NewEnumResponse(ctx, m.Status, AccountStatus_Values),
|
Status: web.NewEnumResponse(ctx, m.Status, AccountStatus_ValuesInterface()...),
|
||||||
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
}
|
}
|
||||||
@ -200,6 +200,15 @@ var AccountStatus_Values = []AccountStatus{
|
|||||||
AccountStatus_Disabled,
|
AccountStatus_Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AccountStatus_ValuesInterface returns the AccountStatus options as a slice interface.
|
||||||
|
func AccountStatus_ValuesInterface() []interface{} {
|
||||||
|
var l []interface{}
|
||||||
|
for _, v := range AccountStatus_Values {
|
||||||
|
l = append(l, v.String())
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// Scan supports reading the AccountStatus value from the database.
|
// Scan supports reading the AccountStatus value from the database.
|
||||||
func (s *AccountStatus) Scan(value interface{}) error {
|
func (s *AccountStatus) Scan(value interface{}) error {
|
||||||
asBytes, ok := value.([]byte)
|
asBytes, ok := value.([]byte)
|
||||||
|
@ -101,11 +101,16 @@ func NewEnumResponse(ctx context.Context, value interface{}, options ...interfac
|
|||||||
|
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
optStr := fmt.Sprintf("%s", opt)
|
optStr := fmt.Sprintf("%s", opt)
|
||||||
er.Options = append(er.Options, EnumOption{
|
opt := EnumOption{
|
||||||
Value: optStr,
|
Value: optStr,
|
||||||
Title: EnumValueTitle(optStr),
|
Title: EnumValueTitle(optStr),
|
||||||
Selected: (value == opt),
|
}
|
||||||
})
|
|
||||||
|
if optStr == er.Value {
|
||||||
|
opt.Selected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
er.Options = append(er.Options, opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return er
|
return er
|
||||||
|
@ -43,7 +43,7 @@ func (m *Project) Response(ctx context.Context) *ProjectResponse {
|
|||||||
ID: m.ID,
|
ID: m.ID,
|
||||||
AccountID: m.AccountID,
|
AccountID: m.AccountID,
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
Status: web.NewEnumResponse(ctx, m.Status, ProjectStatus_Values),
|
Status: web.NewEnumResponse(ctx, m.Status, ProjectStatus_ValuesInterface()...),
|
||||||
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
}
|
}
|
||||||
@ -133,6 +133,15 @@ var ProjectStatus_Values = []ProjectStatus{
|
|||||||
ProjectStatus_Disabled,
|
ProjectStatus_Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProjectStatus_ValuesInterface returns the ProjectStatus options as a slice interface.
|
||||||
|
func ProjectStatus_ValuesInterface() []interface{} {
|
||||||
|
var l []interface{}
|
||||||
|
for _, v := range ProjectStatus_Values {
|
||||||
|
l = append(l, v.String())
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// Scan supports reading the ProjectStatus value from the database.
|
// Scan supports reading the ProjectStatus value from the database.
|
||||||
func (s *ProjectStatus) Scan(value interface{}) error {
|
func (s *ProjectStatus) Scan(value interface{}) error {
|
||||||
asBytes, ok := value.([]byte)
|
asBytes, ok := value.([]byte)
|
||||||
|
@ -53,7 +53,7 @@ func (m *UserAccount) Response(ctx context.Context) *UserAccountResponse {
|
|||||||
UserID: m.UserID,
|
UserID: m.UserID,
|
||||||
AccountID: m.AccountID,
|
AccountID: m.AccountID,
|
||||||
Roles: m.Roles,
|
Roles: m.Roles,
|
||||||
Status: web.NewEnumResponse(ctx, m.Status, UserAccountStatus_Values),
|
Status: web.NewEnumResponse(ctx, m.Status, UserAccountStatus_ValuesInterface()...),
|
||||||
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
}
|
}
|
||||||
@ -169,6 +169,15 @@ var UserAccountStatus_Values = []UserAccountStatus{
|
|||||||
UserAccountStatus_Disabled,
|
UserAccountStatus_Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserAccountStatus_ValuesInterface returns the UserAccountStatus options as a slice interface.
|
||||||
|
func UserAccountStatus_ValuesInterface() []interface{} {
|
||||||
|
var l []interface{}
|
||||||
|
for _, v := range UserAccountStatus_Values {
|
||||||
|
l = append(l, v.String())
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// Scan supports reading the UserAccountStatus value from the database.
|
// Scan supports reading the UserAccountStatus value from the database.
|
||||||
func (s *UserAccountStatus) Scan(value interface{}) error {
|
func (s *UserAccountStatus) Scan(value interface{}) error {
|
||||||
asBytes, ok := value.([]byte)
|
asBytes, ok := value.([]byte)
|
||||||
@ -217,6 +226,15 @@ var UserAccountRole_Values = []UserAccountRole{
|
|||||||
UserAccountRole_User,
|
UserAccountRole_User,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserAccountRole_ValuesInterface returns the UserAccountRole options as a slice interface.
|
||||||
|
func UserAccountRole_ValuesInterface() []interface{} {
|
||||||
|
var l []interface{}
|
||||||
|
for _, v := range UserAccountRole_Values {
|
||||||
|
l = append(l, v.String())
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// String converts the UserAccountRole value to a string.
|
// String converts the UserAccountRole value to a string.
|
||||||
func (s UserAccountRole) String() string {
|
func (s UserAccountRole) String() string {
|
||||||
return string(s)
|
return string(s)
|
||||||
|
Reference in New Issue
Block a user