diff --git a/pkg/database/funcs.go b/pkg/database/funcs.go index d6edf7a..ee30cba 100644 --- a/pkg/database/funcs.go +++ b/pkg/database/funcs.go @@ -131,3 +131,17 @@ func (tx *Tx) TranslateSeason(ctx context.Context, slug string, langTag language _, err := tx.Exec(ctx, "select translate_season($1, $2, $3)", slug, langTag, name) return err } + +func (tx *Tx) AddService(ctx context.Context, companyID int, icon string, name string, description string) (int, error) { + return tx.GetInt(ctx, "select add_service($1, $2, $3, $4)", companyID, icon, name, description) +} + +func (tx *Tx) EditService(ctx context.Context, id int, icon string, name string, description string) error { + _, err := tx.Exec(ctx, "select edit_service($1, $2, $3, $4)", id, icon, name, description) + return err +} + +func (tx *Tx) TranslateService(ctx context.Context, id int, langTag language.Tag, name string, description string) error { + _, err := tx.Exec(ctx, "select translate_service($1, $2, $3, $4)", id, langTag, name, description) + return err +} diff --git a/pkg/form/input.go b/pkg/form/input.go index b6c8d8c..13e12a5 100644 --- a/pkg/form/input.go +++ b/pkg/form/input.go @@ -42,20 +42,6 @@ func (input *Input) Int() int { return i } -func (input *Input) L10nInput() *L10nInput { - return &L10nInput{ - Input: Input{ - Name: input.Name, - }, - Source: input.Val, - } -} - -type L10nInput struct { - Input - Source string -} - type I18nInput map[string]*Input func NewI18nInput(locales locale.Locales, name string) I18nInput { diff --git a/pkg/locale/translation.go b/pkg/locale/translation.go deleted file mode 100644 index 0984983..0000000 --- a/pkg/locale/translation.go +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 jordi fita mas - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package locale - -type Translation struct { - URL string - Endonym string - Missing bool -} diff --git a/pkg/services/admin.go b/pkg/services/admin.go index 0cdbfcb..40dc38a 100644 --- a/pkg/services/admin.go +++ b/pkg/services/admin.go @@ -53,7 +53,7 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat case "new": switch r.Method { case http.MethodGet: - f := newServiceForm(r.Context(), conn) + f := newServiceForm(r.Context(), conn, company) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) @@ -66,7 +66,7 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat http.NotFound(w, r) return } - f := newServiceForm(r.Context(), conn) + f := newServiceForm(r.Context(), conn, company) if err := f.FillFromDatabase(r.Context(), conn, id); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) @@ -75,10 +75,8 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat panic(err) } - var langTag string - langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path) - - switch langTag { + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + switch head { case "": switch r.Method { case http.MethodGet: @@ -91,23 +89,7 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) } default: - loc, ok := h.locales.Get(langTag) - if !ok { - http.NotFound(w, r) - return - } - l10n := newServiceL10nForm(f, loc) - if err := l10n.FillFromDatabase(r.Context(), conn); err != nil { - panic(err) - } - switch r.Method { - case http.MethodGet: - l10n.MustRender(w, r, user, company) - case http.MethodPut: - editServiceL10n(w, r, user, company, conn, l10n) - default: - httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) - } + http.NotFound(w, r) } } }) @@ -136,10 +118,9 @@ func (page *servicesIndex) MustRender(w http.ResponseWriter, r *http.Request, us } type serviceEntry struct { - URL string - Icon string - Name string - Translations []*locale.Translation + URL string + Icon string + Name string } func collectServiceEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*serviceEntry, error) { @@ -147,18 +128,10 @@ func collectServiceEntries(ctx context.Context, company *auth.Company, conn *dat select '/admin/services/' || service_id , icon_name , service.name - , array_agg((lang_tag, endonym, not exists (select 1 from service_i18n as i18n where i18n.service_id = service.service_id and i18n.lang_tag = language.lang_tag))::translation order by endonym) from service - join company using (company_id) - , language - where lang_tag <> default_lang_tag - and language.selectable - and service.company_id = $1 - group by service_id - , icon_name - , service.name + where service.company_id = $1 order by service.name - `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID) + `, company.ID) if err != nil { return nil, err } @@ -167,17 +140,9 @@ func collectServiceEntries(ctx context.Context, company *auth.Company, conn *dat var services []*serviceEntry for rows.Next() { entry := &serviceEntry{} - var translations database.RecordArray - if err = rows.Scan(&entry.URL, &entry.Icon, &entry.Name, &translations); err != nil { + if err = rows.Scan(&entry.URL, &entry.Icon, &entry.Name); err != nil { return nil, err } - for _, el := range translations.Elements { - entry.Translations = append(entry.Translations, &locale.Translation{ - URL: entry.URL + "/" + el.Fields[0].Get().(string), - Endonym: el.Fields[1].Get().(string), - Missing: el.Fields[2].Get().(bool), - }) - } services = append(services, entry) } @@ -185,25 +150,61 @@ func collectServiceEntries(ctx context.Context, company *auth.Company, conn *dat } func addService(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { - f := newServiceForm(r.Context(), conn) + f := newServiceForm(r.Context(), conn, company) + processServiceForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { + var err error + if f.ID, err = tx.AddService(ctx, company.ID, f.Icon.String(), f.Name[f.DefaultLang].Val, f.Description[f.DefaultLang].Val); err != nil { + return err + } + return translateService(ctx, tx, company, f) + }) +} + +func processServiceForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *serviceForm, act func(context.Context, *database.Tx) error) { if ok, err := form.Handle(f, w, r, user); err != nil { return } else if !ok { f.MustRender(w, r, user, company) return } - conn.MustExec(r.Context(), "select add_service($1, $2, $3, $4)", company.ID, f.Icon, f.Name, f.Description) + tx := conn.MustBegin(r.Context()) + if err := act(r.Context(), tx); err == nil { + tx.MustCommit(r.Context()) + } else { + if rErr := tx.Rollback(r.Context()); rErr != nil { + err = rErr + } + panic(err) + } httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther) } +func translateService(ctx context.Context, tx *database.Tx, company *auth.Company, f *serviceForm) error { + for lang := range company.Locales { + l := lang.String() + if l == f.DefaultLang { + continue + } + if err := tx.TranslateService(ctx, f.ID, lang, f.Name[l].Val, f.Description[l].Val); err != nil { + return err + } + } + return nil +} + func editService(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *serviceForm) { + processServiceForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { + if err := tx.EditService(ctx, f.ID, f.Icon.String(), f.Name[f.DefaultLang].Val, f.Description[f.DefaultLang].Val); err != nil { + return err + } + return translateService(ctx, tx, company, f) + }) if ok, err := form.Handle(f, w, r, user); err != nil { return } else if !ok { f.MustRender(w, r, user, company) return } - conn.MustExec(r.Context(), "select edit_service($1, $2, $3, $4)", f.ID, f.Icon, f.Name, f.Description) httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther) } @@ -217,37 +218,52 @@ func deleteService(w http.ResponseWriter, r *http.Request, user *auth.User, conn } type serviceForm struct { + DefaultLang string ID int Icon *form.Select - Name *form.Input - Description *form.Input + Name form.I18nInput + Description form.I18nInput } -func newServiceForm(ctx context.Context, conn *database.Conn) *serviceForm { +func newServiceForm(ctx context.Context, conn *database.Conn, company *auth.Company) *serviceForm { return &serviceForm{ + DefaultLang: company.DefaultLanguage.String(), Icon: &form.Select{ Name: "icon", Options: form.MustGetOptions(ctx, conn, "select icon_name, icon_name from icon order by 1"), }, - Name: &form.Input{ - Name: "name", - }, - Description: &form.Input{ - Name: "description", - }, + Name: form.NewI18nInput(company.Locales, "name"), + Description: form.NewI18nInput(company.Locales, "description"), } } func (f *serviceForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error { f.ID = id + var name database.RecordArray + var description database.RecordArray row := conn.QueryRow(ctx, ` select array[icon_name] - , name - , description + , service.name + , service.description::text + , array_agg((lang_tag, i18n.name)) + , array_agg((lang_tag, i18n.description::text)) from service + left join service_i18n as i18n using (service_id) where service_id = $1 - `, id) - return row.Scan(&f.Icon.Selected, &f.Name.Val, &f.Description.Val) + group by icon_name + , service.name + , service.description::text + `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, id) + if err := row.Scan(&f.Icon.Selected, &f.Name[f.DefaultLang].Val, &f.Description[f.DefaultLang].Val, &name, &description); err != nil { + return err + } + if err := f.Name.FillArray(name); err != nil { + return err + } + if err := f.Description.FillArray(description); err != nil { + return err + } + return nil } func (f *serviceForm) Parse(r *http.Request) error { @@ -263,7 +279,7 @@ func (f *serviceForm) Parse(r *http.Request) error { func (f *serviceForm) Valid(l *locale.Locale) bool { v := form.NewValidator(l) v.CheckSelectedOptions(f.Icon, l.GettextNoop("Selected icon is not valid.")) - v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty.")) + v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty.")) return v.AllOK } diff --git a/pkg/services/l10n.go b/pkg/services/l10n.go deleted file mode 100644 index 8de47bc..0000000 --- a/pkg/services/l10n.go +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 jordi fita mas - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package services - -import ( - "context" - "net/http" - - "dev.tandem.ws/tandem/camper/pkg/auth" - "dev.tandem.ws/tandem/camper/pkg/database" - "dev.tandem.ws/tandem/camper/pkg/form" - httplib "dev.tandem.ws/tandem/camper/pkg/http" - "dev.tandem.ws/tandem/camper/pkg/locale" - "dev.tandem.ws/tandem/camper/pkg/template" -) - -type serviceL10nForm struct { - Locale *locale.Locale - ID int - Name *form.L10nInput - Description *form.L10nInput -} - -func newServiceL10nForm(f *serviceForm, loc *locale.Locale) *serviceL10nForm { - return &serviceL10nForm{ - Locale: loc, - ID: f.ID, - Name: f.Name.L10nInput(), - Description: f.Description.L10nInput(), - } -} - -func (l10n *serviceL10nForm) FillFromDatabase(ctx context.Context, conn *database.Conn) error { - row := conn.QueryRow(ctx, ` - select coalesce(i18n.name, '') as l10n_name - , coalesce(i18n.description, '') as l10n_description - from service - left join service_i18n as i18n on service.service_id = i18n.service_id and i18n.lang_tag = $1 - where service.service_id = $2 - `, l10n.Locale.Language, l10n.ID) - return row.Scan(&l10n.Name.Val, &l10n.Description.Val) -} - -func (l10n *serviceL10nForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { - template.MustRenderAdmin(w, r, user, company, "services/l10n.gohtml", l10n) -} - -func editServiceL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *serviceL10nForm) { - if ok, err := form.Handle(l10n, w, r, user); err != nil { - return - } else if !ok { - l10n.MustRender(w, r, user, company) - return - } - conn.MustExec(r.Context(), "select translate_service($1, $2, $3, $4)", l10n.ID, l10n.Locale.Language, l10n.Name, l10n.Description) - httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther) -} - -func (l10n *serviceL10nForm) Parse(r *http.Request) error { - if err := r.ParseForm(); err != nil { - return err - } - l10n.Name.FillValue(r) - l10n.Description.FillValue(r) - return nil -} - -func (l10n *serviceL10nForm) Valid(l *locale.Locale) bool { - v := form.NewValidator(l) - v.CheckRequired(&l10n.Name.Input, l.GettextNoop("Name can not be empty.")) - return v.AllOK -} diff --git a/web/templates/admin/services/form.gohtml b/web/templates/admin/services/form.gohtml index ab88c73..7c93b38 100644 --- a/web/templates/admin/services/form.gohtml +++ b/web/templates/admin/services/form.gohtml @@ -28,7 +28,7 @@ {{ end }} {{ CSRFInput }} -
+
{{ with $field := .Icon -}}
{{( pgettext "Icon" "input")}} @@ -48,19 +48,30 @@
{{- end }} {{ with .Name -}} - - {{ template "error-message" . }} +
+ {{( pgettext "Name" "input")}} + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
{{- end }} {{ with .Description -}} - - {{ template "error-message" . }} +
+ {{( pgettext "Description" "input")}} + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
{{- end }}