Skip to content
Snippets Groups Projects
Commit 9fb1b265 authored by jensilo's avatar jensilo
Browse files

eiffel elicitation working except form

parent 08f5e611
Branches
No related tags found
No related merge requests found
......@@ -3,6 +3,7 @@ package eiffel
import (
"context"
"errors"
"fmt"
t "github.com/org-harmony/harmony/src/app/template"
"github.com/org-harmony/harmony/src/app/template/parser"
"github.com/org-harmony/harmony/src/core/trans"
......@@ -17,6 +18,10 @@ const BasicTemplateType = "ebt"
var (
// ErrInvalidVariant is an error that is returned when trying to parse a requirement for an invalid variant.
ErrInvalidVariant = errors.New("eiffel.parser.error.invalid-variant")
// ErrNotASlice is an error that is returned when trying to cast an any value to a slice of strings but the value is not a slice.
ErrNotASlice = errors.New("eiffel.parser.error.not-a-slice")
// ErrNotAString is an error that is returned when trying to cast an any value to a string but the value is not a string.
ErrNotAString = errors.New("eiffel.parser.error.not-a-string")
)
// BasicTemplate is the basic EIFFEL template. It is parsable by implementing the template.ParsableTemplate interface.
......@@ -43,9 +48,9 @@ type BasicTemplate struct {
// Description is the description of the template. It is optional.
Description string `json:"description"`
// Format can be used to optionally describe the format of the requirement specified by the template.
Format string `json:"format"`
Format string `json:"format"` // TODO remove this? Format is now defined in the variant.
// Example can be used to optionally provide an example of a requirement specified by the template.
Example string `json:"example"`
Example string `json:"example"` // TODO remove this? Example is now defined in the variant.
// Rules are the rules that can be used in variants to validate requirements.
Rules map[string]BasicRule `json:"rules"`
// Variants are the variants that can be used to validate requirements.
......@@ -112,6 +117,9 @@ type RuleMissingError struct {
// It returns the ErrInvalidRuleValue error on Error(). It may occur during template validation.
type RuleInvalidValueError struct {
Rule *BasicRule
// Msg is the error message. It is optional and may be used to override the default error message.
// The error message is translated using the parameters: "rule" (rule's name) and "type" (rule's type).
Msg string
}
// MissingRuleParserError is an error that is returned when a rule type is not registered in the RuleParserProvider.
......@@ -382,6 +390,10 @@ func (e RuleMissingError) Translate(t trans.Translator) string {
// Error on RuleInvalidValueError returns the error code of the error.
func (e RuleInvalidValueError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "eiffel.parser.error.invalid-rule-value"
}
......@@ -446,9 +458,9 @@ func (p EqualsRuleParser) DisplayType(rule BasicRule) TemplateDisplayType {
// The equalsAny rule expects a slice of strings as value, converts each string to lowercase and compares it to the lowercase segment's value.
// If any of the values are equal, no parsing error is reported.
func (p EqualsAnyRuleParser) Parse(ctx context.Context, rule BasicRule, segment parser.ParsingSegment) ([]parser.ParsingLog, error) {
rv, ok := rule.Value.([]string)
if !ok {
return nil, RuleInvalidValueError{Rule: &rule}
rv, err := toStringSlice(rule.Value)
if err != nil {
return nil, RuleInvalidValueError{Rule: &rule, Msg: err.Error()}
}
segmentValue := strings.ToLower(segment.Value)
......@@ -471,12 +483,12 @@ func (p EqualsAnyRuleParser) Parse(ctx context.Context, rule BasicRule, segment
// Validate implements the RuleParser interface for the EqualsAnyRuleParser. It is used to validate rules of the type 'equalsAny'.
// The equalsAny rule expects a slice of strings as value.
func (p EqualsAnyRuleParser) Validate(v validation.V, rule BasicRule) []error {
_, ok := rule.Value.([]string)
if ok {
_, err := toStringSlice(rule.Value)
if err == nil {
return nil
}
return []error{RuleInvalidValueError{Rule: &rule}}
return []error{RuleInvalidValueError{Rule: &rule, Msg: err.Error()}}
}
// DisplayType implements the RuleParser interface for the EqualsAnyRuleParser. EqualsAny rules are input fields with a single select datalist.
......@@ -521,3 +533,24 @@ func prepareSegments(segments []parser.ParsingSegment) map[string]parser.Parsing
return indexedSegments
}
// toStringSlice cast an any value to a slice of strings. If the value is not a slice of strings, an error is returned.
// This is used by the equalsAny rule parser to cast the rule value to a slice of strings.
// Important: The function does not convert the slice's elements to strings. It only casts the slice to a slice of strings.
func toStringSlice(anyElem any) ([]string, error) {
anySlice, ok := anyElem.([]any)
if !ok {
fmt.Printf("anyElem: %v\n", anyElem)
return nil, ErrNotASlice
}
stringSlice := make([]string, len(anySlice))
for i, v := range anySlice {
stringSlice[i], ok = v.(string)
if !ok {
return nil, ErrNotAString
}
}
return stringSlice, nil
}
package eiffel
import (
"context"
"encoding/json"
"github.com/google/uuid"
"github.com/org-harmony/harmony/src/app/template"
"github.com/org-harmony/harmony/src/core/validation"
)
......@@ -44,3 +46,57 @@ func TemplateIntoBasicTemplate(t *template.Template, validator validation.V, rul
return ebt, nil
}
// TemplateFormFromRequest parses the template and variant from the passed in templateID and variantKey and returns a
// TemplateFormData struct. If the template or variant could not be found, an error is returned.
// However, using the defaultFirstVariant flag, the first variant will be used if no variant was specified and no
// error will be returned. TemplateFormFromRequest will also parse and validate the template.
//
// Returned errors from TemplateFormFromRequest are safe to display to the user.
func TemplateFormFromRequest(
ctx context.Context,
templateID string,
variantKey string,
templateRepository template.Repository,
ruleParsers *RuleParserProvider,
validator validation.V,
defaultFirstVariant bool,
) (TemplateFormData, error) {
templateUUID, err := uuid.Parse(templateID)
if err != nil {
return TemplateFormData{}, ErrTemplateNotFound
}
tmpl, err := templateRepository.FindByID(ctx, templateUUID)
if err != nil {
return TemplateFormData{}, ErrTemplateNotFound
}
bt, err := TemplateIntoBasicTemplate(tmpl, validator, ruleParsers)
if err != nil {
return TemplateFormData{}, err
}
variant, ok := bt.Variants[variantKey]
if !ok && !defaultFirstVariant {
return TemplateFormData{}, ErrTemplateVariantNotFound
}
if !ok {
for n, v := range bt.Variants {
variant = v
variantKey = n
break
}
}
displayTypes := TemplateDisplayTypes(bt, RuleParsers())
return TemplateFormData{
Template: bt,
Variant: &variant,
VariantKey: variantKey,
DisplayTypes: displayTypes,
TemplateID: templateUUID,
}, nil
}
......@@ -3,11 +3,13 @@ package eiffel
import (
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/org-harmony/harmony/src/app/template"
"github.com/org-harmony/harmony/src/app/user"
"github.com/org-harmony/harmony/src/core/event"
"github.com/org-harmony/harmony/src/core/hctx"
"github.com/org-harmony/harmony/src/core/persistence"
"github.com/org-harmony/harmony/src/core/util"
"github.com/org-harmony/harmony/src/core/validation"
"github.com/org-harmony/harmony/src/core/web"
......@@ -39,9 +41,25 @@ type TemplateDisplayType string
// TemplateFormData is the data that is passed to the template rendering the elicitation form.
type TemplateFormData struct {
Template *BasicTemplate
// Variant is the currently selected variant. This might be through a specified variant name parameter
// or as a default value because no variant was explicitly specified. However, Variant is expected to be filled.
Variant *BasicVariant
// VariantKey is the key through which the variant was selected. It is not the name of the variant.
// If the variant was auto-selected as default the key will still be filled.
VariantKey string
// DisplayTypes is a map of rule names to display types. The rule names are the keys of the BasicTemplate.Rules map.
// The display types are used to determine how the rule should be displayed in the UI.
DisplayTypes map[string]TemplateDisplayType
// TemplateID is the ID of the template that is currently being rendered.
TemplateID uuid.UUID
// CopyAfterParse is a flag indicating if the user wants to copy the parsed requirement to the clipboard.
CopyAfterParse bool
}
// SearchTemplateData contains templates to render as search results and a flag indicating if the query was too short.
type SearchTemplateData struct {
Templates []*template.Template
QueryTooShort bool
}
func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx) {
......@@ -53,8 +71,12 @@ func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx) {
router := webCtx.Router.With(user.LoggedInMiddleware(appCtx))
router.Get("/eiffel", eiffelElicitationPage(appCtx, webCtx).ServeHTTP)
router.Get("/eiffel/{templateID}", eiffelElicitationPage(appCtx, webCtx).ServeHTTP)
router.Get("/eiffel/{templateID}/{variant}", eiffelElicitationPage(appCtx, webCtx).ServeHTTP)
router.Get("/eiffel/elicitation/templates/search/modal", searchModal(appCtx, webCtx).ServeHTTP)
router.Get("/eiffel/elicitation/{templateID}/{variant}", elicitationTemplate(appCtx, webCtx).ServeHTTP)
router.Post("/eiffel/elicitation/templates/search", searchTemplate(appCtx, webCtx).ServeHTTP)
router.Get("/eiffel/elicitation/{templateID}", elicitationTemplate(appCtx, webCtx, true).ServeHTTP)
router.Get("/eiffel/elicitation/{templateID}/{variant}", elicitationTemplate(appCtx, webCtx, false).ServeHTTP)
router.Post("/eiffel/elicitation/{templateID}/{variant}", parseRequirement(appCtx, webCtx).ServeHTTP)
router.Get("/eiffel/elicitation/output/file/form", outputFileForm(appCtx, webCtx).ServeHTTP)
}
......@@ -105,16 +127,38 @@ func registerNavigation(appCtx *hctx.AppCtx, webCtx *web.Ctx) {
}
func eiffelElicitationPage(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler {
templateRepository := util.UnwrapType[template.Repository](appCtx.Repository(template.RepositoryName))
return web.NewController(appCtx, webCtx, func(io web.IO) error {
templateID := web.URLParam(io.Request(), "templateID")
variantKey := web.URLParam(io.Request(), "variant")
if templateID == "" {
return renderElicitationPage(io, TemplateFormData{}, nil, nil)
}
formData, err := TemplateFormFromRequest(
io.Context(),
templateID,
variantKey,
templateRepository,
RuleParsers(),
appCtx.Validator,
true,
)
return renderElicitationPage(io, formData, nil, []error{err})
})
}
func renderElicitationPage(io web.IO, data TemplateFormData, success []string, errs []error) error {
return io.Render(
web.NewFormData(TemplateFormData{}, nil),
web.NewFormData(data, success, errs...),
"eiffel.elicitation.page",
"eiffel/elicitation-page.go.html",
"eiffel/elicitation-template.go.html",
"eiffel/_elicitation-template.go.html",
"eiffel/_form-elicitation.go.html",
"eiffel/_form-output-file.go.html",
)
})
}
func searchModal(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler {
......@@ -123,38 +167,66 @@ func searchModal(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler {
})
}
func elicitationTemplate(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler {
func searchTemplate(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler {
templateRepository := util.UnwrapType[template.Repository](appCtx.Repository(template.RepositoryName))
return web.NewController(appCtx, webCtx, func(io web.IO) error {
templateID := web.URLParam(io.Request(), "templateID")
variant := web.URLParam(io.Request(), "variant")
templateUUID, err := uuid.Parse(templateID)
request := io.Request()
err := request.ParseForm()
if err != nil {
return io.InlineError(ErrTemplateNotFound, err)
return io.InlineError(web.ErrInternal, err)
}
tmpl, err := templateRepository.FindByID(io.Context(), templateUUID)
if err != nil {
return io.InlineError(ErrTemplateNotFound, err)
query := request.FormValue("search")
if len(query) < 3 {
return io.Render(
&SearchTemplateData{QueryTooShort: true},
"eiffel.template.search.result",
"eiffel/_template-search-result.go.html",
)
}
bt, err := TemplateIntoBasicTemplate(tmpl, appCtx.Validator, RuleParsers())
if err != nil {
return io.InlineError(err)
templates, err := templateRepository.FindByQueryForType(io.Context(), query, BasicTemplateType)
if err != nil && !errors.Is(err, persistence.ErrNotFound) {
return io.InlineError(web.ErrInternal, err)
}
if _, ok := bt.Variants[variant]; !ok {
return io.InlineError(ErrTemplateVariantNotFound)
return io.Render(
&SearchTemplateData{Templates: templates},
"eiffel.template.search.result",
"eiffel/_template-search-result.go.html",
)
})
}
func elicitationTemplate(appCtx *hctx.AppCtx, webCtx *web.Ctx, defaultFirstVariant bool) http.Handler {
templateRepository := util.UnwrapType[template.Repository](appCtx.Repository(template.RepositoryName))
return web.NewController(appCtx, webCtx, func(io web.IO) error {
templateID := web.URLParam(io.Request(), "templateID")
variant := web.URLParam(io.Request(), "variant")
io.Response().Header().Set("HX-Push-URL", fmt.Sprintf("/eiffel/%s", templateID))
formData, err := TemplateFormFromRequest(
io.Context(),
templateID,
variant,
templateRepository,
RuleParsers(),
appCtx.Validator,
defaultFirstVariant,
)
if err != nil {
return io.InlineError(err)
}
displayTypes := TemplateDisplayTypes(bt, RuleParsers())
io.Response().Header().Set("HX-Push-URL", fmt.Sprintf("/eiffel/%s/%s", templateID, formData.VariantKey))
return io.Render(
web.NewFormData(TemplateFormData{Template: bt, DisplayTypes: displayTypes}, nil),
web.NewFormData(formData, nil),
"eiffel.elicitation.template",
"eiffel/elicitation-template.go.html",
"eiffel/_elicitation-template.go.html",
"eiffel/_form-elicitation.go.html",
)
})
......
......@@ -40,6 +40,7 @@ type Template struct {
CreatedBy uuid.UUID
CreatedAt time.Time
UpdatedAt *time.Time
TemplateSetElem *Set
}
// ToCreate is the template entity that is used to create a new template.
......@@ -109,6 +110,11 @@ type PGSetRepository struct {
type Repository interface {
persistence.Repository
// FindByQueryForType finds all templates by a query for a specified template type.
// The query will be searched for in the template's name, version and in the template set's name.
// It will join the template.Set onto template.Template and read it into Set.TemplateSetElem.
// It returns persistence.ErrNotFound if no templates could be found and persistence.ErrReadRow for any other error.
FindByQueryForType(ctx context.Context, query string, templateType string) ([]*Template, error)
// 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.
FindByID(ctx context.Context, id uuid.UUID) (*Template, error)
// 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.
......@@ -215,6 +221,40 @@ func (r *PGSetRepository) RepositoryName() string {
return SetRepositoryName
}
// FindByQueryForType finds all templates by a query for a specified template type.
// It returns persistence.ErrNotFound if no templates could be found and persistence.ErrReadRow for any other error.
func (r *PGRepository) FindByQueryForType(ctx context.Context, query string, templateType string) ([]*Template, error) {
rows, err := r.db.Query(
ctx,
`SELECT
templates.id, templates.template_set, templates.type, templates.name, templates.version, templates.config, templates.created_by, templates.created_at, templates.updated_at,
template_sets.name, template_sets.version, template_sets.description, template_sets.created_by, template_sets.created_at, template_sets.updated_at
FROM templates LEFT JOIN template_sets ON templates.template_set = template_sets.id
WHERE templates.name ILIKE $1 OR templates.version ILIKE $1 OR template_sets.name ILIKE $1 AND templates.type = $2`,
"%"+query+"%",
templateType,
)
if err != nil {
return nil, persistence.PGReadErr(err)
}
var templates []*Template
for rows.Next() {
t := &Template{TemplateSetElem: &Set{}}
err := rows.Scan(
&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Config, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt,
&t.TemplateSetElem.Name, &t.TemplateSetElem.Version, &t.TemplateSetElem.Description, &t.TemplateSetElem.CreatedBy, &t.TemplateSetElem.CreatedAt, &t.TemplateSetElem.UpdatedAt,
)
if err != nil {
return nil, persistence.PGReadErr(err)
}
templates = append(templates, t)
}
return templates, nil
}
// 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{}
......
{{ define "eiffel.elicitation.template" }}
{{ if .Data.Form.Template }}
<div class="accordion mt-3 eiffel-elicitation-template-info" id="eiffelTemplateInfoAccordion">
{{ $templateID := .Data.Form.TemplateID }}
{{ $rules := .Data.Form.Template.Rules }}
{{ $displayTypes := .Data.Form.DisplayTypes }}
{{ $variantKey := .Data.Form.VariantKey }}
<div class="eiffel-elicitation-template-variant mt-3 bg-light rounded p-3 w-100 m-auto border border-light-subtle">
<div class="row">
<div class="col">
{{ range $key, $variant := .Data.Form.Template.Variants }}
<input id="eiffelVariant-{{ $key }}"
hx-get="/eiffel/elicitation/{{ $templateID }}/{{ $key }}"
hx-target="#eiffelElicitationTemplate"
type="radio" name="options-base"
{{ if eq $variantKey $key }}checked{{ end }}
autocomplete="off" class="btn-check"/>
<label class="btn" for="eiffelVariant-{{ $key }}">{{ $variant.Name }}</label>
{{ end }}
</div>
</div>
<div class="row mt-2">
<div class="col d-flex justify-content-between">
<span class="badge shadow rounded-pill text-bg-secondary">{{ t "eiffel.elicitation.template.variant.left.shortcut" }}</span>
<span class="badge shadow rounded-pill text-bg-secondary">{{ t "eiffel.elicitation.template.variant.right.shortcut" }}</span>
</div>
</div>
</div>
<div class="accordion mt-5 eiffel-elicitation-template-info" id="eiffelTemplateInfoAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<h2 class="accordion-header" id="headingConstruction">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseConstruction" aria-expanded="true" aria-controls="collapseConstruction">
{{ t "eiffel.elicitation.template.construction" }}
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#eiffelTemplateInfoAccordion">
<div id="collapseConstruction" class="accordion-collapse collapse show" aria-labelledby="headingConstruction" data-bs-parent="#eiffelTemplateInfoAccordion">
<div class="accordion-body">
Hier stehen mehr Informationen zur Schablone. Lorem Ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nisl quis aliquam ultricies, nunc nisl aliquet nunc, sit amet aliquam nisl nunc sit amet nisl. Sed euismod, nisl quis aliquam ultricies, nunc nisl aliquet nunc, sit amet aliquam nisl nunc sit amet nisl. Sed euismod, nisl quis aliquam ultricies, nunc nisl aliquet nunc, sit amet aliquam nisl nunc sit amet nisl. Sed euismod, nisl quis aliquam ultricies, nunc nisl aliquet nunc, sit amet aliquam nisl nunc sit amet nisl.
{{ if .Data.Form.Variant.Format }}
{{ .Data.Form.Variant.Format }}
{{ else }}
{{ range .Data.Form.Variant.Rules }}
{{ $rule := index $rules . }}
<span class="eiffel-elicitation-template-info-rule">
{{ if eq (index $displayTypes .) "text" }}
{{ $rule.Value }}
{{ else }}
{{ if $rule.Optional }}
[{{ $rule.Name }}]
{{ else }}
<{{ $rule.Name }}>
{{ end }}
{{ end }}
</span>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ if .Data.Form.Variant.Example }}
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<h2 class="accordion-header" id="headingExample">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
{{ t "eiffel.elicitation.template.example" }}
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#eiffelTemplateInfoAccordion">
<div id="collapseExample" class="accordion-collapse collapse" aria-labelledby="headingExample" data-bs-parent="#eiffelTemplateInfoAccordion">
<div class="accordion-body">
{{ .Data.Form.Variant.Example }}
</div>
</div>
</div>
{{ end }}
{{ if or .Data.Form.Variant.Description .Data.Form.Template.Description }}
<div class="accordion-item">
<h2 class="accordion-header" id="headingDesc">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDesc" aria-expanded="false" aria-controls="collapseDesc">
{{ t "eiffel.elicitation.template.description.title" }}
</button>
</h2>
<div id="collapseDesc" class="accordion-collapse collapse" aria-labelledby="headingDesc" data-bs-parent="#eiffelTemplateInfoAccordion">
<div class="accordion-body">
Hier stehen mehr Informationen zur Schablone. Lorem Ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nisl quis aliquam ultricies, nunc nisl aliquet nunc, sit amet aliquam nisl nunc sit amet nisl. Sed euismod, nisl quis aliquam ultricies, nunc nisl aliquet nunc, sit amet aliquam nisl nunc sit amet nisl. Sed euismod, nisl quis aliquam ultricies, nunc nisl aliquet nunc, sit amet aliquam nisl nunc sit amet nisl. Sed euismod, nisl quis aliquam ultricies, nunc nisl aliquet nunc, sit amet aliquam nisl nunc sit amet nisl.
<dl class="mb-0">
{{ if .Data.Form.Variant.Description }}
<dt>{{ t "eiffel.elicitation.template.variant.description" }}</dt>
<dd>{{ .Data.Form.Variant.Description }}</dd>
{{ end }}
{{ if .Data.Form.Template.Description }}
<dt>{{ t "eiffel.elicitation.template.description" }}</dt>
<dd>{{ .Data.Form.Template.Description }}</dd>
{{ end }}
</dl>
</div>
</div>
</div>
{{ end }}
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<h2 class="accordion-header" id="headingSettings">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSettings" aria-expanded="false" aria-controls="collapseSettings">
{{ t "eiffel.elicitation.template.settings" }}
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#eiffelTemplateInfoAccordion">
<div id="collapseSettings" class="accordion-collapse collapse" aria-labelledby="headingSettings" data-bs-parent="#eiffelTemplateInfoAccordion">
<div class="accordion-body">
Hier ein paar kleinere Einstellungen.
<div class="form-check">
<input form="eiffelElicitationForm" class="form-check-input" type="checkbox"
name="copyAfterParse" id="copyAfterParse"
{{ if .Data.Form.CopyAfterParse }}checked{{ end }}/>
<label class="form-check-label" for="copyAfterParse">
{{ t "eiffel.elicitation.template.copy-after-parse" }}
</label>
</div>
</div>
</div>
</div>
......@@ -51,33 +127,21 @@
zurückgegeben. Die aktuell ausgewählte Schablone und Variante sollten mit übergeben werden (über die URL).
Die ausgewählte Datei sollte über einen Input außerhalb des Formulars übergeben werden. (Hier kann das Input-Attribut "form" verwendet werden.)
*/}}
<div class="eiffel-elicitation-template-variant mt-5 bg-light rounded p-3 w-100 m-auto border border-light-subtle">
<div class="row">
<div class="col">
<input type="radio" class="btn-check" name="options-base" id="option1" autocomplete="off"/>
<label class="btn" for="option1">Variante 1</label>
<input type="radio" class="btn-check" name="options-base" id="option2" autocomplete="off"/>
<label class="btn" for="option2">Variante 2</label>
<input type="radio" class="btn-check" name="options-base" id="option3" autocomplete="off" checked/>
<label class="btn" for="option3">Variante 3</label>
</div>
</div>
<div class="row mt-2">
<div class="col d-flex justify-content-between">
<span class="badge shadow rounded-pill text-bg-secondary">{{ t "eiffel.elicitation.template.variant.left.shortcut" }}</span>
<span class="badge shadow rounded-pill text-bg-secondary">{{ t "eiffel.elicitation.template.variant.right.shortcut" }}</span>
</div>
</div>
</div>
<div class="eiffel-elicitation-template-variant-form mt-3 w-100">
{{ template "eiffel.elicitation.form" . }}
</div>
{{ else }}
{{ if not .Data.Valid }}
{{ range .Data.AllViolations }}
<div class="alert alert-danger mt-3" role="alert">
{{ t .Error }}
</div>
{{ end }}
{{ else }}
<div class="alert alert-info mt-3" role="alert">
{{ t "eiffel.elicitation.template.search.call-to-action" }}
</div>
{{ end }}
{{ end }}
{{ end }}
\ No newline at end of file
{{ define "eiffel.template.search.modal" }}
<div class="modal-dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="eiffelTemplateSearchLabel">{{ t "eiffel.elicitation.template.search.title" }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ t "harmony.generic.close" }}"></button>
</div>
<div class="modal-body">
\here goes the search form\
<div class="mb-3">
<input id="eiffelTemplateSearchInput"
name="search" type="search" class="form-control"
hx-post="/eiffel/elicitation/templates/search"
hx-trigger="input changed delay:300ms, search"
hx-target="#eiffelTemplateSearchResults"
hx-disabled-elt="#eiffelTemplateSearchInput"
aria-label="{{ t "eiffel.elicitation.template.search.placeholder" }}"
placeholder="{{ t "eiffel.elicitation.template.search.placeholder" }}"/>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">{{ t "template.title" }}</th>
<th scope="col">{{ t "template.version" }}</th>
<th scope="col">{{ t "template.set.title" }}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="eiffelTemplateSearchResults">
<tr>
<td class="text-center" colspan="4">{{ t "eiffel.elicitation.template.search.start" }}</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t "harmony.generic.close" }}</button>
......
{{ define "eiffel.template.search.result" }}
{{ if .Data.Templates }}
{{ range .Data.Templates }}
<tr>
<td>{{ .Name }}</td>
<td>{{ .Version }}</td>
<td>{{ .TemplateSetElem.Name }}</td>
<td>
<button hx-get="/eiffel/elicitation/{{ .ID }}"
hx-target="#eiffelElicitationTemplate"
data-bs-dismiss="modal"
class="btn btn-primary btn-sm">
{{ t "eiffel.elicitation.template.search.select" }}
</button>
</td>
</tr>
{{ end }}
{{ else if .Data.QueryTooShort }}
<tr>
<td colspan="4" class="text-center">{{ t "eiffel.elicitation.template.search.query-too-short" }}</td>
</tr>
{{ else }}
<tr>
<td colspan="4" class="text-center">{{ t "eiffel.elicitation.template.search.not-found" }}</td>
</tr>
{{ end }}
{{ end }}
\ No newline at end of file
......@@ -55,9 +55,11 @@
</div>*/}}
</div>
<div id="eiffelElicitationTemplate">
{{ template "eiffel.elicitation.template" . }}
</div>
</div>
</div>
<div class="col eiffel-requirements">
<p>Requirements here</p>
......
......@@ -22,6 +22,7 @@
},
"template": {
"set": {
"title": "Schablonensatz",
"list": "Schablonensätze Übersicht",
"list.empty": "Es wurden noch keine Schablonensätze erstellt.",
"new": "Neuen Schablonensatz erstellen",
......@@ -53,6 +54,7 @@
"updated": "Der Schablonensatz wurde aktualisiert."
}
},
"title": "Schablone",
"list": "Schablonen Übersicht {{ .name }}",
"list.empty": "Es wurden noch keine Schablonen erstellt.",
"new": {
......@@ -89,7 +91,9 @@
"invalid-variant": "Die Variante ist ungültig.",
"missing-rule-parser": "Der geforderte Parser für die Regel ist nicht verfügbar. Wahrscheinlich ist der Regel-Typ \"{{ .type }}\" nicht korrekt. Bitte überprüfen Sie die Schablonen-Dokumentation.",
"missing-segment": "Eine Eingabe für die Regel \"{{ .name }}\" ({{ .technicalName }}) fehlt.",
"invalid-rule-value": "Der Wert \"value\" für die Regel \"{{ .rule }}\" vom Typ \"{{ .type }}\" ist ungültig. Bitte überprüfen Sie die Schablonen-Dokumentation."
"invalid-rule-value": "Der Wert \"value\" für die Regel \"{{ .rule }}\" vom Typ \"{{ .type }}\" ist ungültig. Bitte überprüfen Sie die Schablonen-Dokumentation.",
"not-a-slice": "Der Wert \"value\" für die Regel \"{{ .rule }}\" vom Typ \"{{ .type }}\" ist keine Liste. Bitte überprüfen Sie die Schablonen-Dokumentation.",
"not-a-string": "Der Wert \"value\" für die Regel \"{{ .rule }}\" vom Typ \"{{ .type }}\" sollte aus einer Zeichenkette oder einer Liste an Zeichenketten bestehen, jedoch wurde ein anderer Typ gefunden. Bitte überprüfen Sie die Schablonen-Dokumentation."
}
},
"elicitation": {
......@@ -97,21 +101,29 @@
"search": {
"title": "Schablone suchen",
"shortcut": "Strg + F",
"select": "Auswählen",
"placeholder": "Schablonen durchsuchen - Namen hier eingeben...",
"start": "Suchen Sie im Eingabefeld nach einer Schablone und wählen Sie diese für die Erfassung aus.",
"call-to-action": "Es wurde noch keine Schablone ausgewählt. Bitte nutzen Sie die Suche, um eine Schablone zu finden.",
"not-found": "Keine Schablone gefunden."
"not-found": "Keine Schablonen gefunden.",
"query-too-short": "Geben Sie mindestens 3 Zeichen ein, um die Suche zu starten."
},
"not-found": "Die Schablone wurde nicht gefunden.",
"variant": {
"left.shortcut": "<- + Strg",
"right.shortcut": "Strg + ->",
"not-found": "Die Variante wurde nicht gefunden."
"not-found": "Die Variante wurde nicht gefunden.",
"description": "Variantenbeschreibung"
},
"form": {
"title": "Anforderung erfassen"
},
"construction": "Schablonenaufbau",
"example": "Beispielsatz",
"settings": "Einstellungen"
"description.title": "Beschreibung",
"description": "Schablonenbeschreibung",
"settings": "Einstellungen",
"copy-after-parse": "Anforderung nach erfolgreicher Prüfung kopieren"
}
}
},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment