Deletes CPA values on CPA field type change (#31122)

* Deletes CPA values on CPA field type change

* Fix error method name reference

* Cleans the state when a CPA field's type is updated

* Fix types

* Fix linter

* Webapp no longer makes a decision on the change and server sents a flag in the WS message

* Fix linter

---------

Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Этот коммит содержится в:
Miguel de la Cruz
2025-05-27 13:38:05 +02:00
коммит произвёл GitHub
родитель b3649132d0
Коммит e51ea025db
15 изменённых файлов: 684 добавлений и 61 удалений

Просмотреть файл

@@ -10,6 +10,7 @@ import (
"sort"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/pkg/errors"
)
@@ -123,6 +124,11 @@ func (a *App) PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*m
return nil, appErr
}
shouldDeleteValues := false
if patch.Type != nil && *patch.Type != existingField.Type {
shouldDeleteValues = true
}
// custom profile attributes doesn't use targets
patch.TargetID = nil
patch.TargetType = nil
@@ -130,28 +136,41 @@ func (a *App) PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*m
cpaField, err := model.NewCPAFieldFromPropertyField(existingField)
if err != nil {
return nil, model.NewAppError("UpdateCPAField", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return nil, model.NewAppError("PatchCPAField", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := cpaField.SanitizeAndValidate(); appErr != nil {
return nil, appErr
}
// we've already ensured that the field exists for the CPA group,
// we don't need to specify the groupID for the update
patchedField, err := a.Srv().propertyService.UpdatePropertyField("", cpaField.ToPropertyField())
groupID, err := a.CpaGroupID()
if err != nil {
return nil, model.NewAppError("PatchCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
patchedField, err := a.Srv().propertyService.UpdatePropertyField(groupID, cpaField.ToPropertyField())
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
return nil, model.NewAppError("PatchCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("UpdateCPAField", "app.custom_profile_attributes.property_field_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return nil, model.NewAppError("PatchCPAField", "app.custom_profile_attributes.property_field_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if shouldDeleteValues {
if dErr := a.Srv().propertyService.DeletePropertyValuesForField(groupID, patchedField.ID); dErr != nil {
a.Log().Error("Error deleting property values when updating field",
mlog.String("fieldID", patchedField.ID),
mlog.Err(dErr),
)
}
}
message := model.NewWebSocketEvent(model.WebsocketEventCPAFieldUpdated, "", "", "", nil, "")
message.Add("field", patchedField)
message.Add("delete_values", shouldDeleteValues)
a.Publish(message)
return patchedField, nil

Просмотреть файл

@@ -409,6 +409,134 @@ func TestPatchCPAField(t *testing.T) {
require.Equal(t, "New Option 1.5", updatedOptions[1].Name)
require.Equal(t, "#353535", updatedOptions[1].Color)
})
t.Run("Should not delete the values of a field after patching it if the type has not changed", func(t *testing.T) {
// Create a select field with options
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
GroupID: cpaGroupID,
Name: "Select Field with values",
Type: model.PropertyFieldTypeSelect,
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{
"name": "Option 1",
"color": "#FF5733",
},
map[string]any{
"name": "Option 2",
"color": "#33FF57",
},
},
},
})
require.NoError(t, err)
createdField, appErr := th.App.CreateCPAField(field)
require.Nil(t, appErr)
// Get the option IDs by converting back to CPA field
cpaField, err := model.NewCPAFieldFromPropertyField(createdField)
require.NoError(t, err)
options := cpaField.Attrs.Options
require.Len(t, options, 2)
optionID := options[0].ID
require.NotEmpty(t, optionID)
// Create values for this field using the first option
userID := model.NewId()
value, appErr := th.App.PatchCPAValue(userID, createdField.ID, json.RawMessage(fmt.Sprintf(`"%s"`, optionID)), false)
require.Nil(t, appErr)
require.NotNil(t, value)
// Patch the field without changing type (just update name and add a new option)
patch := &model.PropertyFieldPatch{
Name: model.NewPointer("Updated select field name"),
Attrs: model.NewPointer(model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{
"id": optionID, // Keep the same ID for the first option
"name": "Updated Option 1",
"color": "#FF5733",
},
map[string]any{
"name": "Option 2",
"color": "#33FF57",
},
map[string]any{
"name": "Option 3",
"color": "#5733FF",
},
},
}),
}
updatedField, appErr := th.App.PatchCPAField(createdField.ID, patch)
require.Nil(t, appErr)
require.Equal(t, "Updated select field name", updatedField.Name)
require.Equal(t, model.PropertyFieldTypeSelect, updatedField.Type)
// Verify values still exist
values, appErr := th.App.ListCPAValues(userID)
require.Nil(t, appErr)
require.Len(t, values, 1)
require.Equal(t, json.RawMessage(fmt.Sprintf(`"%s"`, optionID)), values[0].Value)
})
t.Run("Should delete the values of a field after patching it if the type has changed", func(t *testing.T) {
// Create a select field with options
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
GroupID: cpaGroupID,
Name: "Select Field with type change",
Type: model.PropertyFieldTypeSelect,
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{
"name": "Option A",
"color": "#FF5733",
},
map[string]any{
"name": "Option B",
"color": "#33FF57",
},
},
},
})
require.NoError(t, err)
createdField, appErr := th.App.CreateCPAField(field)
require.Nil(t, appErr)
// Get the option IDs by converting back to CPA field
cpaField, err := model.NewCPAFieldFromPropertyField(createdField)
require.NoError(t, err)
options := cpaField.Attrs.Options
require.Len(t, options, 2)
optionID := options[0].ID
require.NotEmpty(t, optionID)
// Create values for this field
userID := model.NewId()
value, appErr := th.App.PatchCPAValue(userID, createdField.ID, json.RawMessage(fmt.Sprintf(`"%s"`, optionID)), false)
require.Nil(t, appErr)
require.NotNil(t, value)
// Verify value exists before type change
values, appErr := th.App.ListCPAValues(userID)
require.Nil(t, appErr)
require.Len(t, values, 1)
// Patch the field and change type from select to text
patch := &model.PropertyFieldPatch{
Type: model.NewPointer(model.PropertyFieldTypeText),
}
updatedField, appErr := th.App.PatchCPAField(createdField.ID, patch)
require.Nil(t, appErr)
require.Equal(t, model.PropertyFieldTypeText, updatedField.Type)
// Verify values have been deleted
values, appErr = th.App.ListCPAValues(userID)
require.Nil(t, appErr)
require.Empty(t, values)
})
}
func TestDeleteCPAField(t *testing.T) {

Просмотреть файл

@@ -59,7 +59,7 @@ func (ps *PropertyService) DeletePropertyField(groupID, id string) error {
}
}
if err := ps.valueStore.DeleteForField(id); err != nil {
if err := ps.valueStore.DeleteForField(groupID, id); err != nil {
return err
}

Просмотреть файл

@@ -60,3 +60,7 @@ func (ps *PropertyService) DeletePropertyValue(groupID, id string) error {
func (ps *PropertyService) DeletePropertyValuesForTarget(groupID string, targetType string, targetID string) error {
return ps.valueStore.DeleteForTarget(groupID, targetType, targetID)
}
func (ps *PropertyService) DeletePropertyValuesForField(groupID, fieldID string) error {
return ps.valueStore.DeleteForField(groupID, fieldID)
}