diff --git a/migrations/Init1697574747_down.sql b/migrations/Init1697574747_down.sql index 0441b6b2d5c501794e4b3f545c910f90ff6f7c79..51616bf012cd71027a9d2fdef0d864a254f61450 100644 --- a/migrations/Init1697574747_down.sql +++ b/migrations/Init1697574747_down.sql @@ -1,7 +1,7 @@ -DROP TABLE IF EXISTS users; - DROP TABLE IF EXISTS sessions; DROP TABLE IF EXISTS templates; DROP TABLE IF EXISTS template_sets; + +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/migrations/Init1697574747_up.sql b/migrations/Init1697574747_up.sql index 4c8df60d3f91917eb3849f4b4e7c2025cbb23311..91d6902d7458b9c587dd5bb02e42d88ffee7bdf2 100644 --- a/migrations/Init1697574747_up.sql +++ b/migrations/Init1697574747_up.sql @@ -37,7 +37,7 @@ CREATE TABLE templates type VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, version VARCHAR(255) NOT NULL, - json JSONB NOT NULL, + config JSONB NOT NULL, created_by UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, updated_at TIMESTAMPTZ diff --git a/public/assets/icons/refresh.svg b/public/assets/icons/refresh.svg new file mode 100644 index 0000000000000000000000000000000000000000..b072eb097abd3c493a1371d82a6cca349ef63d41 --- /dev/null +++ b/public/assets/icons/refresh.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/> + <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/> +</svg> \ No newline at end of file diff --git a/src/app/eiffel/parser.go b/src/app/eiffel/parser.go index 1710137d486f777d95a417fcd46553a1a0be509b..0951d2c2cc66f0a02b576147111a3c7e68026717 100644 --- a/src/app/eiffel/parser.go +++ b/src/app/eiffel/parser.go @@ -8,7 +8,17 @@ import ( const BasicTemplateName = "ebt" -var ErrInvalidTemplate = errors.New("eiffel.parser.error.invalid-template") +var ( + ErrInvalidTemplate = errors.New("eiffel.parser.error.invalid-template") +) + +type ParsingResult struct { + Template string + Variant string + Errors []error + Warnings []error + Notices []error +} type BasicParser struct { Template *BasicTemplate `hvalidate:"required,ruleReferences"` @@ -58,6 +68,14 @@ type PreprocessorMissingError struct { type BasicPreprocessor func(string) (string, error) +func (p *BasicParser) Parse(input string, variant string) (string, error) { + /*v, ok := p.Template.Variants[variant] + if !ok { + return "", errors.New("eiffel.parser.error.variant-not-found") + }*/ + return "", nil +} + func (p *BasicParser) Validate(v validation.V) []error { v.AddFunc("ruleReferences", RuleReferencesValidator) diff --git a/src/app/template/template.go b/src/app/template/template.go index 95a023708f750346499109583d295543dae9f378..6d1c582429474c967801f5dfcc67bb6d5e82e3e0 100644 --- a/src/app/template/template.go +++ b/src/app/template/template.go @@ -17,11 +17,11 @@ const ( SetRepositoryName = "SetRepository" ) -// ErrTemplateJsonMissingInfo is returned if the template's JSON does not contain the necessary information (name and version). -var ErrTemplateJsonMissingInfo = errors.New("template json missing necessary information (check name and version)") +// ErrTemplateConfigMissingInfo is returned if the template's config JSON does not contain the necessary information (name and version). +var ErrTemplateConfigMissingInfo = errors.New("template's config json missing necessary information (check name and version)") // Template is the template entity that is saved in the database. It contains the template's metadata. -// Each template belongs to a template set. Templates are versioned and the information about the template should always match the template's JSON. +// Each template belongs to a template set. Templates are versioned and the information about the template should always match the template's config JSON. // Actually, Type, Name and Version are redundant, but they are used for easier querying. type Template struct { ID uuid.UUID @@ -29,7 +29,7 @@ type Template struct { Type string Name string Version string - Json string + Config string CreatedBy uuid.UUID CreatedAt time.Time UpdatedAt *time.Time @@ -39,7 +39,7 @@ type Template struct { type ToCreate struct { TemplateSet uuid.UUID `hvalidate:"required"` Type string `hvalidate:"required"` - Json string `hvalidate:"required"` + Config string `hvalidate:"required"` CreatedBy uuid.UUID `hvalidate:"required"` } @@ -48,11 +48,11 @@ type ToUpdate struct { ID uuid.UUID `hvalidate:"required"` TemplateSet uuid.UUID `hvalidate:"required"` Type string `hvalidate:"required"` - Json string `hvalidate:"required"` + Config string `hvalidate:"required"` } // NecessaryInfo is the necessary information about a template. It is used to create a new template. -// The template's JSON has to contain this information. It is extracted from the JSON and saved in the database. +// The template's config JSON has to contain this information. It is extracted from the config JSON and saved in the database. type NecessaryInfo struct { Name string `json:"name"` Version string `json:"version"` @@ -106,12 +106,12 @@ type Repository interface { // FindByTemplateSetID finds all templates by their template set id. It returns persistence.ErrNotFound if no templates could be found and persistence.ErrReadRow for any other error. FindByTemplateSetID(ctx context.Context, templateSetID uuid.UUID) ([]*Template, error) // Create creates a new template and returns it. It returns persistence.ErrInsert if the template could not be inserted. - // It also extracts the necessary information from the template's JSON and saves it in the database. - // If the JSON does not contain the necessary information, it returns ErrTemplateJsonMissingInfo. + // It also extracts the necessary information from the template's config JSON and saves it in the database. + // If the config JSON does not contain the necessary information, it returns ErrTemplateConfigMissingInfo. Create(ctx context.Context, template *ToCreate) (*Template, error) // Update updates an existing template and returns it. It returns persistence.ErrUpdate if the template could not be updated. - // It also extracts the necessary information from the template's JSON and saves it in the database. - // If the JSON does not contain the necessary information, it returns ErrTemplateJsonMissingInfo. + // It also extracts the necessary information from the template's config JSON and saves it in the database. + // If the config JSON does not contain the necessary information, it returns ErrTemplateConfigMissingInfo. Update(ctx context.Context, template *ToUpdate) (*Template, error) // Delete deletes an existing template by its id. It returns persistence.ErrDelete if the template could not be deleted. Delete(ctx context.Context, id uuid.UUID) error @@ -140,19 +140,19 @@ func (t *Template) ToUpdate() *ToUpdate { ID: t.ID, TemplateSet: t.TemplateSet, Type: t.Type, - Json: t.Json, + Config: t.Config, } } // NecessaryInfo returns the valid necessary information about a template from a Template. -// It will return ErrTemplateJsonMissingInfo if the template's JSON does not contain the necessary information (name and version). -// This method is used by Created and Update to extract the necessary information from the template's JSON. +// It will return ErrTemplateConfigMissingInfo if the template's config JSON does not contain the necessary information (name and version). +// This method is used by Created and Update to extract the necessary information from the template's config JSON. func (t *Template) NecessaryInfo() (*NecessaryInfo, error) { info := &NecessaryInfo{} - err := json.Unmarshal([]byte(t.Json), info) + err := json.Unmarshal([]byte(t.Config), info) if info.Name == "" || info.Version == "" { - return nil, ErrTemplateJsonMissingInfo + return nil, ErrTemplateConfigMissingInfo } return info, err @@ -191,8 +191,8 @@ func (r *PGSetRepository) RepositoryName() string { // FindByID finds a template by its id. It returns persistence.ErrNotFound if the template could not be found and persistence.ErrReadRow for any other error. func (r *PGRepository) FindByID(ctx context.Context, id uuid.UUID) (*Template, error) { t := &Template{} - err := r.db.QueryRow(ctx, "SELECT id, template_set, type, name, version, json, created_by, created_at, updated_at FROM templates WHERE id = $1", id). - Scan(&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Json, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt) + err := r.db.QueryRow(ctx, "SELECT id, template_set, type, name, version, config, created_by, created_at, updated_at FROM templates WHERE id = $1", id). + Scan(&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Config, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt) if err != nil { return nil, persistence.PGReadErr(err) @@ -203,7 +203,7 @@ func (r *PGRepository) FindByID(ctx context.Context, id uuid.UUID) (*Template, e // FindByTemplateSetID finds all templates by their template set id. It returns persistence.ErrNotFound if no templates could be found and persistence.ErrReadRow for any other error. func (r *PGRepository) FindByTemplateSetID(ctx context.Context, templateSetID uuid.UUID) ([]*Template, error) { - rows, err := r.db.Query(ctx, "SELECT id, template_set, type, name, version, json, created_by, created_at, updated_at FROM templates WHERE template_set = $1", templateSetID) + rows, err := r.db.Query(ctx, "SELECT id, template_set, type, name, version, config, created_by, created_at, updated_at FROM templates WHERE template_set = $1", templateSetID) if err != nil { return nil, persistence.PGReadErr(err) } @@ -211,7 +211,7 @@ func (r *PGRepository) FindByTemplateSetID(ctx context.Context, templateSetID uu var templates []*Template for rows.Next() { t := &Template{} - err := rows.Scan(&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Json, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt) + err := rows.Scan(&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Config, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt) if err != nil { return nil, persistence.PGReadErr(err) } @@ -223,14 +223,14 @@ func (r *PGRepository) FindByTemplateSetID(ctx context.Context, templateSetID uu } // Create creates a new template and returns it. It returns persistence.ErrInsert if the template could not be inserted. -// It also checks if the template's JSON contains the necessary information (name and version). -// If the JSON does not contain the necessary information, it returns ErrTemplateJsonMissingInfo. +// It also checks if the template's config JSON contains the necessary information (name and version). +// If the config JSON does not contain the necessary information, it returns ErrTemplateConfigMissingInfo. func (r *PGRepository) Create(ctx context.Context, toCreate *ToCreate) (*Template, error) { newTemplate := &Template{ ID: uuid.New(), TemplateSet: toCreate.TemplateSet, Type: toCreate.Type, - Json: toCreate.Json, + Config: toCreate.Config, CreatedBy: toCreate.CreatedBy, CreatedAt: time.Now(), } @@ -245,8 +245,8 @@ func (r *PGRepository) Create(ctx context.Context, toCreate *ToCreate) (*Templat _, err = r.db.Exec( ctx, - "INSERT INTO templates (id, template_set, name, version, type, json, created_by, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", - newTemplate.ID, newTemplate.TemplateSet, newTemplate.Name, newTemplate.Version, newTemplate.Type, newTemplate.Json, newTemplate.CreatedBy, newTemplate.CreatedAt, + "INSERT INTO templates (id, template_set, name, version, type, config, created_by, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + newTemplate.ID, newTemplate.TemplateSet, newTemplate.Name, newTemplate.Version, newTemplate.Type, newTemplate.Config, newTemplate.CreatedBy, newTemplate.CreatedAt, ) if err != nil { return nil, errors.Join(persistence.ErrInsert, err) @@ -256,12 +256,12 @@ func (r *PGRepository) Create(ctx context.Context, toCreate *ToCreate) (*Templat } // Update updates an existing template and returns it. It returns persistence.ErrUpdate if the template could not be updated. -// It also checks if the template's JSON contains the necessary information (name and version). -// If the JSON does not contain the necessary information, it returns ErrTemplateJsonMissingInfo. +// It also checks if the template's config JSON contains the necessary information (name and version). +// If the config JSON does not contain the necessary information, it returns ErrTemplateConfigMissingInfo. func (r *PGRepository) Update(ctx context.Context, toUpdate *ToUpdate) (*Template, error) { template := &Template{ - ID: toUpdate.ID, - Json: toUpdate.Json, + ID: toUpdate.ID, + Config: toUpdate.Config, } tmplInfo, err := template.NecessaryInfo() @@ -272,16 +272,16 @@ func (r *PGRepository) Update(ctx context.Context, toUpdate *ToUpdate) (*Templat err = r.db.QueryRow( ctx, `UPDATE templates - SET template_set = $1, type = $2, name = $3, version = $4, json = $5, updated_at = NOW() + SET template_set = $1, type = $2, name = $3, version = $4, config = $5, updated_at = NOW() WHERE id = $6 - RETURNING template_set, type, name, version, json, created_by, created_at, updated_at`, - toUpdate.TemplateSet, toUpdate.Type, tmplInfo.Name, tmplInfo.Version, toUpdate.Json, toUpdate.ID, + RETURNING template_set, type, name, version, config, created_by, created_at, updated_at`, + toUpdate.TemplateSet, toUpdate.Type, tmplInfo.Name, tmplInfo.Version, toUpdate.Config, toUpdate.ID, ).Scan( &template.TemplateSet, &template.Type, &template.Name, &template.Version, - &template.Json, + &template.Config, &template.CreatedBy, &template.CreatedAt, &template.UpdatedAt, diff --git a/src/app/template/template_test.go b/src/app/template/template_test.go index ee0b888326ce8ec04a4a53e6e6ab8172d4c04b0d..67e59e3a5d520439775430ab1bfaa468c86dada8 100644 --- a/src/app/template/template_test.go +++ b/src/app/template/template_test.go @@ -50,14 +50,14 @@ func TestPGRepository(t *testing.T) { found, err := templateRepo.FindByID(ctx, tmpl.ID) require.NoError(t, err) require.NotNil(t, tmpl) - unifiedJsonEqual(t, tmpl.Json, found.Json) + unifiedConfigEqual(t, tmpl.Config, found.Config) assert.Equal(t, tmplUnify(*tmpl), tmplUnify(*found)) }) t.Run("FindByTemplateSet", func(t *testing.T) { tmplToCreate := &ToCreate{ Type: "ebt", - Json: `{ + Config: `{ "name": "Baz", "version": "1.0.0", "authors": ["Qux Bar"], @@ -80,7 +80,7 @@ func TestPGRepository(t *testing.T) { t.Run("Create Template", func(t *testing.T) { tmplToCreate := &ToCreate{ Type: "ebt", - Json: `{ + Config: `{ "name": "Baz", "version": "1.0.0", "authors": ["Qux Bar"], @@ -103,7 +103,7 @@ func TestPGRepository(t *testing.T) { assert.Equal(t, tmpl.Version, "1.0.0") assert.Equal(t, tmpl.TemplateSet, tmplSet.ID) assert.Equal(t, tmpl.CreatedBy, u.ID) - unifiedJsonEqual(t, tmplToCreate.Json, tmpl.Json) + unifiedConfigEqual(t, tmplToCreate.Config, tmpl.Config) }) t.Run("Update Template", func(t *testing.T) { @@ -117,7 +117,7 @@ func TestPGRepository(t *testing.T) { toUpdate := newTmpl.ToUpdate() toUpdate.Type = "foo" - toUpdate.Json = `{ + toUpdate.Config = `{ "name": "Bizzo", "version": "2.0.0", "authors": ["Qux Bar"], @@ -133,7 +133,7 @@ func TestPGRepository(t *testing.T) { assert.Equal(t, update.Type, "foo") assert.Equal(t, update.Name, "Bizzo") assert.Equal(t, update.Version, "2.0.0") - unifiedJsonEqual(t, toUpdate.Json, update.Json) + unifiedConfigEqual(t, toUpdate.Config, update.Config) }) t.Run("Delete Template", func(t *testing.T) { @@ -292,7 +292,7 @@ func fooToCreate() (*user.ToCreate, *SetToCreate, *ToCreate) { Description: "Foo Bar", }, &ToCreate{ Type: "ebt", - Json: `{ + Config: `{ "name": "Foo", "version": "1.0.0", "authors": ["Foo Bar"], @@ -303,10 +303,10 @@ func fooToCreate() (*user.ToCreate, *SetToCreate, *ToCreate) { } // tmplUnify unifies the template for comparison. -// It sets the json to "{}" as different whitespaces may lead to different json strings while the content is identical. +// It sets the config json to "{}" as different whitespaces may lead to different json strings while the content is identical. // It truncates the time to seconds as the database does not store milliseconds. func tmplUnify(tmpl Template) Template { - tmpl.Json = "{}" + tmpl.Config = "{}" tmpl.CreatedAt = tmpl.CreatedAt.Truncate(time.Second) if tmpl.UpdatedAt != nil { *tmpl.UpdatedAt = tmpl.UpdatedAt.Truncate(time.Second) @@ -315,18 +315,18 @@ func tmplUnify(tmpl Template) Template { return tmpl } -// unifiedJsonEqual compares two json strings by unmarshalling them into a map[string]any. -// Even with different whitespaces the json strings are considered equal if the content is equal. -func unifiedJsonEqual(t *testing.T, expected string, actual string) { - expectedJson := make(map[string]any) - actualJson := make(map[string]any) +// unifiedConfigEqual compares two config json strings by unmarshalling them into a map[string]any. +// Even with different whitespaces the config json strings are considered equal if the content is equal. +func unifiedConfigEqual(t *testing.T, expectedJson string, actualJson string) { + expectedConfig := make(map[string]any) + actualConfig := make(map[string]any) - err := json.Unmarshal([]byte(expected), &expectedJson) + err := json.Unmarshal([]byte(expectedJson), &expectedConfig) require.NoError(t, err) - err = json.Unmarshal([]byte(actual), &actualJson) + err = json.Unmarshal([]byte(actualJson), &actualConfig) require.NoError(t, err) - assert.Equal(t, expectedJson, actualJson) + assert.Equal(t, expectedConfig, actualConfig) } // tmplSetUnify unifies the template set for comparison. It truncates the time to seconds as the database does not store milliseconds. diff --git a/src/app/template/web.go b/src/app/template/web.go index de7c9f7d34053ad171800fd280ce36f56374a2e9..5e84ac1c50692616f4ad8c6902c7da07754735c9 100644 --- a/src/app/template/web.go +++ b/src/app/template/web.go @@ -12,6 +12,12 @@ import ( "net/http" ) +var ( + ErrInvalidTemplateSetID = errors.New("invalid template set id") + ErrTemplateSetNotFound = errors.New("template set not found") + ErrUserNotPermitted = errors.New("user not permitted") +) + func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx) { registerNavigation(appCtx, webCtx) @@ -23,6 +29,37 @@ func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx) { router.Get("/template-set/edit/{id}", templateSetEditFormController(appCtx, webCtx).ServeHTTP) router.Put("/template-set/{id}", templateSetEditController(appCtx, webCtx).ServeHTTP) router.Delete("/template-set/{id}", templateSetDeleteController(appCtx, webCtx).ServeHTTP) + + router.Get("/template-set/{id}/list", templateListController(appCtx, webCtx).ServeHTTP) + router.Get("/template-set/{id}/new", templateNewController(appCtx, webCtx).ServeHTTP) + router.Post("/template-set/{id}/new", templateNewSaveController(appCtx, webCtx).ServeHTTP) +} + +// SetFromParams returns a template set from the given request parameters. It might return an error if +// the template set id is invalid (ErrInvalidTemplateSetID), the template set is not found (ErrTemplateSetNotFound) +// or the user is not permitted to access the template set (ErrUserNotPermitted). +// In the latter case, the template set is still returned and the caller can decide whether to handle the user +// not being permitted to access this template set as an error or not. +func SetFromParams(io web.IO, repo SetRepository, param string) (*Set, error) { + ctx := io.Context() + u := user.MustCtxUser(ctx) + + templateSetID := web.URLParam(io.Request(), param) + templateSetUUID, err := uuid.Parse(templateSetID) + if templateSetID == "" || err != nil { + return nil, ErrInvalidTemplateSetID + } + + templateSet, err := repo.FindByID(ctx, templateSetUUID) + if err != nil { + return nil, errors.Join(ErrTemplateSetNotFound, err) + } + + if templateSet.CreatedBy != u.ID { + return templateSet, ErrUserNotPermitted + } + + return templateSet, nil } func registerNavigation(appCtx *hctx.AppCtx, webCtx *web.Ctx) { @@ -92,25 +129,11 @@ func templateSetEditFormController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Ha templateSetRepository := util.UnwrapType[SetRepository](appCtx.Repository(SetRepositoryName)) return web.NewController(appCtx, webCtx, func(io web.IO) error { - ctx := io.Context() - u := user.MustCtxUser(ctx) - - templateSetID := web.URLParam(io.Request(), "id") - templateSetUUID, err := uuid.Parse(templateSetID) - if templateSetID == "" || err != nil { - return io.InlineError(web.ErrInternal, fmt.Errorf("template set id %s invalid (during edit page)", templateSetID), err) - } - - templateSet, err := templateSetRepository.FindByID(ctx, templateSetUUID) + templateSet, err := SetFromParams(io, templateSetRepository, "id") if err != nil { return io.InlineError(web.ErrInternal, err) } - if templateSet.CreatedBy != u.ID { - appCtx.Info("user %s tried to edit template set %s without permission", u.ID.String(), templateSetID) - return io.InlineError(web.ErrInternal, fmt.Errorf("template set %s not found", templateSetID)) - } - return io.RenderJoined( web.NewFormData(templateSet.ToUpdate(), nil), "template.set.edit.form", @@ -124,24 +147,12 @@ func templateSetEditController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handle return web.NewController(appCtx, webCtx, func(io web.IO) error { ctx := io.Context() - u := user.MustCtxUser(ctx) - - templateSetID := web.URLParam(io.Request(), "id") - templateSetUUID, err := uuid.Parse(templateSetID) - if templateSetID == "" || err != nil { - return io.InlineError(web.ErrInternal, fmt.Errorf("template set id %s invalid (during edit)", templateSetID), err) - } - templateSet, err := templateSetRepository.FindByID(ctx, templateSetUUID) + templateSet, err := SetFromParams(io, templateSetRepository, "id") if err != nil { return io.InlineError(web.ErrInternal, err) } - if templateSet.CreatedBy != u.ID { - appCtx.Info("user %s tried to edit template set %s without permission", u.ID.String(), templateSetID) - return io.InlineError(web.ErrInternal, fmt.Errorf("template set %s not found", templateSetID)) - } - toUpdate := templateSet.ToUpdate() err, validationErrs := web.ReadForm(io.Request(), toUpdate, appCtx.Validator) if err != nil { @@ -168,23 +179,12 @@ func templateSetDeleteController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Hand ctx := io.Context() u := user.MustCtxUser(ctx) - templateSetID := web.URLParam(io.Request(), "id") - templateSetUUID, err := uuid.Parse(templateSetID) - if templateSetID == "" || err != nil { - return io.InlineError(web.ErrInternal, fmt.Errorf("template set id %s invalid (during deletion)", templateSetID), err) - } - - templateSet, err := templateSetRepository.FindByID(ctx, templateSetUUID) + templateSet, err := SetFromParams(io, templateSetRepository, "id") if err != nil { return io.InlineError(web.ErrInternal, err) } - if templateSet.CreatedBy != u.ID { - appCtx.Info("user %s tried to delete template set %s without permission", u.ID.String(), templateSetID) - return io.InlineError(web.ErrInternal, fmt.Errorf("template set %s not found", templateSetID)) - } - - err = templateSetRepository.Delete(ctx, templateSetUUID) + err = templateSetRepository.Delete(ctx, templateSet.ID) if err != nil { return io.InlineError(web.ErrInternal, err) } @@ -197,3 +197,99 @@ func templateSetDeleteController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Hand return io.Render("template.set.list", "template/_list-set.go.html", templateSets) }) } + +func templateListController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler { + templateSetRepository := util.UnwrapType[SetRepository](appCtx.Repository(SetRepositoryName)) + templateRepository := util.UnwrapType[Repository](appCtx.Repository(RepositoryName)) + + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + + templateSet, err := SetFromParams(io, templateSetRepository, "id") + if err != nil { + return io.Error(web.ErrInternal, err) + } + + templates, err := templateRepository.FindByTemplateSetID(ctx, templateSet.ID) + if err != nil { + return io.Error(web.ErrInternal, err) + } + + return io.RenderJoined(struct { + TemplateSet *Set + Templates []*Template + }{ + TemplateSet: templateSet, + Templates: templates, + }, "template.list.page", "template/list-page.go.html", "template/_list.go.html") + }) +} + +func templateNewController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler { + templateSetRepository := util.UnwrapType[SetRepository](appCtx.Repository(SetRepositoryName)) + + return web.NewController(appCtx, webCtx, func(io web.IO) error { + templateSet, err := SetFromParams(io, templateSetRepository, "id") + if err != nil { + return io.Error(web.ErrInternal, err) + } + + return io.RenderJoined( + web.NewFormData(struct { + TemplateSet *Set + Template *ToCreate + }{ + TemplateSet: templateSet, + Template: &ToCreate{TemplateSet: templateSet.ID}, + }, nil), + "template.new.page", + "template/new-page.go.html", + "template/_form-new.go.html", + ) + }) +} + +func templateNewSaveController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler { + templateSetRepository := util.UnwrapType[SetRepository](appCtx.Repository(SetRepositoryName)) + templateRepository := util.UnwrapType[Repository](appCtx.Repository(RepositoryName)) + + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + u := user.MustCtxUser(ctx) + + templateSet, err := SetFromParams(io, templateSetRepository, "id") + if err != nil { + return io.Error(web.ErrInternal, err) + } + + // todo add validation and transformation based on template type => e.g. EBT (EIFFEL Basic Template) + + toCreate := &ToCreate{TemplateSet: templateSet.ID, CreatedBy: u.ID} + err, validationErrs := web.ReadForm(io.Request(), toCreate, appCtx.Validator) + if err != nil { + return io.Error(web.ErrInternal, err) + } + + if validationErrs != nil { + return io.RenderJoined( + web.NewFormData(struct { + TemplateSet *Set + Template *ToCreate + }{ + TemplateSet: templateSet, + Template: toCreate, + }, nil, validationErrs...), + "template.new.page", + "template/new-page.go.html", + "template/_form-new.go.html", + ) + } + + _, err = templateRepository.Create(ctx, toCreate) + if err != nil { + return io.Error(web.ErrInternal, err) + } + + return io.Redirect(fmt.Sprintf("/template-set/%s/list", templateSet.ID), http.StatusFound) + }) +} diff --git a/templates/template/_form-new.go.html b/templates/template/_form-new.go.html new file mode 100644 index 0000000000000000000000000000000000000000..8322007b0761121acbfc772194bfb97f7c369f86 --- /dev/null +++ b/templates/template/_form-new.go.html @@ -0,0 +1,39 @@ +{{ define "template.new.form" }} + {{ printf "%+v" .Data }} + <div class="card template-new-form-card"> + <div class="card-header">{{ t "template.new" }}</div> + <div class="card-body"> + <form method="post" action="/template-set/{{ .Data.Form.TemplateSet.ID }}/new"> + <fieldset class="template-new-fieldset"> + <div id="form-messages"> + {{ range $success := .Data.Successes }} + <div class="alert alert-success">{{ t $success }}</div> + {{ end }} + {{ range $violation := .Data.WildcardViolations }} + <div class="alert alert-danger">{{ t $violation.Error }}</div> + {{ end }} + </div> + + <div class="row"> + <div class="col-12"> + <label for="config" class="form-label">{{ t "template.config" }}</label> + <textarea + rows="10" + id="config" + class="form-control {{ if .Data.HasViolations "Config" }}is-invalid{{ end }}" + name="Config" + placeholder="{{ t "template.config" }}" + >{{ .Data.Form.Template.Config }}</textarea> + {{ range $validation := .Data.ValidationErrors "Config" }} + <div class="invalid-feedback">{{ t $validation.GenericErrorKey }}</div> + {{ end }} + </div> + <div class="col mt-2"> + <button type="submit" class="btn btn-primary">{{ t "harmony.generic.create" }}</button> + </div> + </div> + </fieldset> + </form> + </div> + </div> +{{ end }} \ No newline at end of file diff --git a/templates/template/_form-set-new.go.html b/templates/template/_form-set-new.go.html index 601dce5bd82cbf4977a6e914031dbc1ee6affd52..2a0a330f0e3a9c2956be765351fdd2208d392317 100644 --- a/templates/template/_form-set-new.go.html +++ b/templates/template/_form-set-new.go.html @@ -57,7 +57,7 @@ {{ end }} </div> <div class="col mt-2"> - <button type="submit" class="btn btn-primary">Submit</button> + <button type="submit" class="btn btn-primary">{{ t "harmony.generic.create" }}</button> </div> </div> </fieldset> diff --git a/templates/template/_list-set.go.html b/templates/template/_list-set.go.html index 021353c43a1d1b03ba5f28b64d790f6d7ca41cbf..f4dca73933625ed7e9416d9aee555787032d1310 100644 --- a/templates/template/_list-set.go.html +++ b/templates/template/_list-set.go.html @@ -1,12 +1,20 @@ {{ define "template.set.list" }} <div class="template-set-list"> <div class="template-set-list-header row mb-5"> - <div class="col-8"> + <div class="col-7"> <h1>{{ "template.set.list" | t }}</h1> </div> <div class="col"> <a href="/template-set/new" hx-boost="true" hx-target="body" class="btn btn-secondary">{{ "template.set.new" | t }}</a> </div> + <div class="col"> + <button hx-get="/template-set/list" hx-target="body" class="btn btn-secondary"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/> + <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/> + </svg> + </button> + </div> </div> <table class="table"> <thead> @@ -25,7 +33,7 @@ {{ range .Data }} <tr> - <td><a class="template-set-view" href="/template-set/{{ .ID }}" hx-boost="true" hx-target="body">{{ .Name }}</a></td> + <td><a class="template-set-view" href="/template-set/{{ .ID }}/list" hx-boost="true" hx-target="body">{{ .Name }}</a></td> <td>{{ .Version }}</td> <td> {{/* edit button + modal */}} diff --git a/templates/template/_list.go.html b/templates/template/_list.go.html new file mode 100644 index 0000000000000000000000000000000000000000..25cf81afa8aef08ec30aa154a1592d1848ef9679 --- /dev/null +++ b/templates/template/_list.go.html @@ -0,0 +1,41 @@ +{{ define "template.list" }} + <div class="template-list"> + <div class="template-set-list-header row mb-5"> + <div class="col-7"> + <h1>{{ tf "template.list" "name" .Data.TemplateSet.Name }}</h1> + </div> + <div class="col"> + <a href="/template-set/{{ .Data.TemplateSet.ID }}/new" hx-boost="true" hx-target="body" class="btn btn-secondary">{{ "template.new" | t }}</a> + </div> + <div class="col"> + <button hx-get="/template-set/{{ .Data.TemplateSet.ID }}/list" hx-target="body" class="btn btn-secondary"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/> + <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/> + </svg> + </button> + </div> + </div> + </div> + <div class="template-list-set-information"> + <div class="card"> + <div class="card-header">{{ "template.set.information" | t }}</div> + <div class="card-body"> + <dl class="row"> + <dt class="col-4">{{ "template.set.name" | t }}</dt> + <dd class="col-8">{{ .Data.TemplateSet.Name }}</dd> + <dt class="col-4">{{ "template.set.version" | t }}</dt> + <dd class="col-8">{{ .Data.TemplateSet.Version }}</dd> + <dt class="col-4">{{ "template.set.description" | t }}</dt> + <dd class="col-8">{{ .Data.TemplateSet.Description }}</dd> + <dt class="col-4">{{ "template.set.createdAt" | t }}</dt> + <dd class="col-8">{{ .Data.TemplateSet.CreatedAt.Format "02.01.2006" }}</dd> + {{ if .Data.TemplateSet.UpdatedAt }} + <dt class="col-sm-4">{{ "template.set.updatedAt" | t }}</dt> + <dd class="col-sm-8">{{ .Data.TemplateSet.UpdatedAt.Format "02.01.2006" }}</dd> + {{ end }} + </dl> + </div> + </div> + </div> +{{ end }} \ No newline at end of file diff --git a/templates/template/list-page.go.html b/templates/template/list-page.go.html new file mode 100644 index 0000000000000000000000000000000000000000..1b59249e0c639d16cb270f1e77d5b66fc0537952 --- /dev/null +++ b/templates/template/list-page.go.html @@ -0,0 +1,7 @@ +{{ define "template.list.page" }} + {{ template "index" . }} +{{ end }} + +{{ define "content" }} + {{ template "template.list" . }} +{{ end }} \ No newline at end of file diff --git a/templates/template/new-page.go.html b/templates/template/new-page.go.html new file mode 100644 index 0000000000000000000000000000000000000000..0e86defeaa064d3d962a2c7d3bff8165fed9a326 --- /dev/null +++ b/templates/template/new-page.go.html @@ -0,0 +1,7 @@ +{{ define "template.new.page" }} + {{ template "index" . }} +{{ end }} + +{{ define "content" }} + {{ template "template.new.form" . }} +{{ end }} \ No newline at end of file diff --git a/templates/user/_form-edit.go.html b/templates/user/_form-edit.go.html index bb02fd8a98e20906afd894d62d8ce3e5cd94ca0f..23462b833b0a5b6dd622e14d4216b641e45903fb 100644 --- a/templates/user/_form-edit.go.html +++ b/templates/user/_form-edit.go.html @@ -52,7 +52,7 @@ {{ end }} </div> <div class="col mt-2"> - <button type="submit" class="btn btn-primary">Submit</button> + <button type="submit" class="btn btn-primary">{{ t "harmony.generic.save" }}</button> </div> </div> </fieldset> diff --git a/translations/de.json b/translations/de.json index 89dffccabc393940ee6046549bd3919bf814c72b..a1b187ef9cfd970ef6feb0cdcf2a20b75fc977df 100644 --- a/translations/de.json +++ b/translations/de.json @@ -25,9 +25,12 @@ "list": "Schablonensätze Übersicht", "list.empty": "Es wurden noch keine Schablonensätze erstellt.", "new": "Neuen Schablonensatz erstellen", + "information": "Informationen", "name": "Name", "version": "Version", "description": "Beschreibung", + "createdAt": "Erstellt am", + "updatedAt": "Zuletzt aktualisiert am", "action": { "actions": "Aktionen", "edit": "Bearbeiten", @@ -50,6 +53,17 @@ "cancel": "Abbrechen", "updated": "Der Schablonensatz wurde aktualisiert." } + }, + "list": "Schablonen Übersicht {{ .name }}", + "list.empty": "Es wurden noch keine Schablonen erstellt.", + "new": "Neue Schablone erstellen", + "name": "Name", + "version": "Version", + "config": "Konfiguration (JSON)", + "action": { + "actions": "Aktionen", + "edit": "Bearbeiten", + "delete": "Löschen" } }, "eiffel": { @@ -97,7 +111,10 @@ } }, "generic": { - "close": "Schließen" + "close": "Schließen", + "refresh": "Aktualisieren", + "save": "Speichern", + "create": "Erstellen" } } } \ No newline at end of file