From c20a8b5a68dcb9fdede92e707461e3302e7013c4 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Sat, 5 Jul 2025 02:49:33 +1200 Subject: [PATCH] fix(template): app template v3 error [BE-11998] (#854) --- api/http/handler/templates/utils_fetch_templates.go | 6 ++++-- .../CreateView/tests/app-templates.test.tsx | 5 +---- .../DeployFormWidget/EnvVarsFieldset.test.tsx | 11 +++++++---- .../DeployFormWidget/EnvVarsFieldset.tsx | 12 +++++++----- .../DeployFormWidget/StackDeployForm/types.ts | 2 +- .../portainer/templates/app-templates/view-model.ts | 4 +--- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/api/http/handler/templates/utils_fetch_templates.go b/api/http/handler/templates/utils_fetch_templates.go index 73f9bad56..fc5c97125 100644 --- a/api/http/handler/templates/utils_fetch_templates.go +++ b/api/http/handler/templates/utils_fetch_templates.go @@ -40,11 +40,13 @@ func (handler *Handler) fetchTemplates() (*listResponse, *httperror.HandlerError } defer resp.Body.Close() - err = json.NewDecoder(resp.Body).Decode(&body) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return nil, httperror.InternalServerError("Unable to parse template file", err) } + for i := range body.Templates { + body.Templates[i].ID = portainer.TemplateID(i + 1) + } return body, nil } diff --git a/app/react/edge/edge-stacks/CreateView/tests/app-templates.test.tsx b/app/react/edge/edge-stacks/CreateView/tests/app-templates.test.tsx index 9cb4ba546..a0d13e43a 100644 --- a/app/react/edge/edge-stacks/CreateView/tests/app-templates.test.tsx +++ b/app/react/edge/edge-stacks/CreateView/tests/app-templates.test.tsx @@ -80,10 +80,7 @@ test('The form should submit the correct request body for a given app template', // fill in the name and select the docker edge group const user = userEvent.setup(); await user.type(getByRole('textbox', { name: 'Name *' }), 'my-stack'); - await user.type( - getByRole('textbox', { name: 'License key *' }), - 'license-123' - ); + await user.type(getByRole('textbox', { name: 'License key' }), 'license-123'); const selectElement = getByLabelText('Edge groups'); await selectEvent.select(selectElement, 'docker'); diff --git a/app/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset.test.tsx b/app/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset.test.tsx index d5dbb2633..12c836d0d 100644 --- a/app/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset.test.tsx +++ b/app/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset.test.tsx @@ -51,7 +51,7 @@ test('calls onChange when input value changes', async () => { const inputElement = screen.getByDisplayValue(value.VAR1); await user.clear(inputElement); - expect(onChange).toHaveBeenCalledWith({ VAR1: '' }); + expect(onChange).toHaveBeenCalledWith({ VAR1: undefined }); const newValue = 'New Value'; await user.type(inputElement, newValue); @@ -107,11 +107,14 @@ test('validates env vars fieldset', () => { ]); const validData = { VAR1: 'Value 1', VAR2: 'Value 2' }; - const invalidData = { VAR1: '', VAR2: 'Value 2' }; + const emptyData = { VAR1: '', VAR2: 'Value 2' }; + const undefinedData = { VAR1: undefined, VAR2: 'Value 2' }; const validResult = schema.isValidSync(validData); - const invalidResult = schema.isValidSync(invalidData); + const emptyResult = schema.isValidSync(emptyData); + const undefinedResult = schema.isValidSync(undefinedData); expect(validResult).toBe(true); - expect(invalidResult).toBe(false); + expect(emptyResult).toBe(true); + expect(undefinedResult).toBe(true); }); diff --git a/app/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset.tsx b/app/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset.tsx index dad33af07..a1281772f 100644 --- a/app/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset.tsx +++ b/app/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset.tsx @@ -6,7 +6,7 @@ import { TemplateEnv } from '@/react/portainer/templates/app-templates/types'; import { FormControl } from '@@/form-components/FormControl'; import { Input, Select } from '@@/form-components/Input'; -type Value = Record; +type Value = Record; export { type Value as EnvVarsValue }; @@ -27,7 +27,7 @@ export function EnvVarsFieldset({ handleChange(env.name, value)} errors={errors?.[env.name]} /> @@ -36,7 +36,7 @@ export function EnvVarsFieldset({ ); function handleChange(name: string, envValue: string) { - onChange({ ...values, [name]: envValue }); + onChange({ ...values, [name]: envValue || undefined }); } } @@ -55,7 +55,7 @@ function Item({ return ( @@ -101,7 +101,9 @@ export function envVarsFieldsetValidation( ): SchemaOf { return object( Object.fromEntries( - definitions.map((v) => [v.name, string().required('Required')]) + definitions + .filter((v) => !v.preset) + .map((v) => [v.name, string().optional()]) ) ); } diff --git a/app/react/portainer/templates/app-templates/DeployFormWidget/StackDeployForm/types.ts b/app/react/portainer/templates/app-templates/DeployFormWidget/StackDeployForm/types.ts index d94a2412e..f40d79c56 100644 --- a/app/react/portainer/templates/app-templates/DeployFormWidget/StackDeployForm/types.ts +++ b/app/react/portainer/templates/app-templates/DeployFormWidget/StackDeployForm/types.ts @@ -2,6 +2,6 @@ import { AccessControlFormData } from '@/react/portainer/access-control/types'; export interface FormValues { name: string; - envVars: Record; + envVars: Record; accessControl: AccessControlFormData; } diff --git a/app/react/portainer/templates/app-templates/view-model.ts b/app/react/portainer/templates/app-templates/view-model.ts index 94ea24bc3..3619caa20 100644 --- a/app/react/portainer/templates/app-templates/view-model.ts +++ b/app/react/portainer/templates/app-templates/view-model.ts @@ -88,10 +88,8 @@ function setTemplatesV3(this: TemplateViewModel, template: AppTemplate) { this.Id = template.id; } -let templateV2ID = 0; - function setTemplatesV2(this: TemplateViewModel, template: AppTemplate) { - this.Id = templateV2ID++; + this.Id = template.id; this.Title = template.title; this.Type = template.type; this.Description = template.description;