From cc71d7f1138c9d8152b04dad1b589bb744a33edc Mon Sep 17 00:00:00 2001 From: memoclaw <265580040+memoclaw@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:15:30 +0800 Subject: [PATCH] fix: allow tags metadata without background color --- proto/api/v1/instance_service.proto | 3 +- proto/gen/api/v1/instance_service.pb.go | 3 +- proto/gen/openapi.yaml | 4 +- proto/gen/store/instance_setting.pb.go | 3 +- proto/store/instance_setting.proto | 3 +- server/router/api/v1/instance_service.go | 9 ++- .../api/v1/test/instance_service_test.go | 28 +++++++++ store/test/instance_setting_test.go | 28 +++++++++ web/src/components/Settings/TagsSection.tsx | 59 ++++++++++--------- web/src/locales/en.json | 5 +- .../types/proto/api/v1/instance_service_pb.ts | 3 +- 11 files changed, 106 insertions(+), 42 deletions(-) diff --git a/proto/api/v1/instance_service.proto b/proto/api/v1/instance_service.proto index 6dc8f9fb2..1f500b3e3 100644 --- a/proto/api/v1/instance_service.proto +++ b/proto/api/v1/instance_service.proto @@ -167,7 +167,8 @@ message InstanceSetting { // Metadata for a tag. message TagMetadata { - // Background color for the tag label. + // Optional background color for the tag label. + // When unset, the default tag color is used. google.type.Color background_color = 1; // Whether memos with this tag should have their content blurred. bool blur_content = 2; diff --git a/proto/gen/api/v1/instance_service.pb.go b/proto/gen/api/v1/instance_service.pb.go index 37d48328c..be77651a0 100644 --- a/proto/gen/api/v1/instance_service.pb.go +++ b/proto/gen/api/v1/instance_service.pb.go @@ -759,7 +759,8 @@ func (x *InstanceSetting_MemoRelatedSetting) GetReactions() []string { // Metadata for a tag. type InstanceSetting_TagMetadata struct { state protoimpl.MessageState `protogen:"open.v1"` - // Background color for the tag label. + // Optional background color for the tag label. + // When unset, the default tag color is used. BackgroundColor *color.Color `protobuf:"bytes,1,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"` // Whether memos with this tag should have their content blurred. BlurContent bool `protobuf:"varint,2,opt,name=blur_content,json=blurContent,proto3" json:"blur_content,omitempty"` diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index 6a6901410..a315fcf95 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -2396,7 +2396,9 @@ components: backgroundColor: allOf: - $ref: '#/components/schemas/Color' - description: Background color for the tag label. + description: |- + Optional background color for the tag label. + When unset, the default tag color is used. blurContent: type: boolean description: Whether memos with this tag should have their content blurred. diff --git a/proto/gen/store/instance_setting.pb.go b/proto/gen/store/instance_setting.pb.go index d51be76ae..b12889b40 100644 --- a/proto/gen/store/instance_setting.pb.go +++ b/proto/gen/store/instance_setting.pb.go @@ -754,7 +754,8 @@ func (x *InstanceMemoRelatedSetting) GetReactions() []string { type InstanceTagMetadata struct { state protoimpl.MessageState `protogen:"open.v1"` - // Background color for the tag label. + // Optional background color for the tag label. + // When unset, the default tag color is used. BackgroundColor *color.Color `protobuf:"bytes,1,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"` // Whether memos with this tag should have their content blurred. BlurContent bool `protobuf:"varint,2,opt,name=blur_content,json=blurContent,proto3" json:"blur_content,omitempty"` diff --git a/proto/store/instance_setting.proto b/proto/store/instance_setting.proto index a6701bd7a..2c7848d3c 100644 --- a/proto/store/instance_setting.proto +++ b/proto/store/instance_setting.proto @@ -111,7 +111,8 @@ message InstanceMemoRelatedSetting { } message InstanceTagMetadata { - // Background color for the tag label. + // Optional background color for the tag label. + // When unset, the default tag color is used. google.type.Color background_color = 1; // Whether memos with this tag should have their content blurred. bool blur_content = 2; diff --git a/server/router/api/v1/instance_service.go b/server/router/api/v1/instance_service.go index 4be6986b7..f6e6d519c 100644 --- a/server/router/api/v1/instance_service.go +++ b/server/router/api/v1/instance_service.go @@ -423,11 +423,10 @@ func validateInstanceTagsSetting(setting *v1pb.InstanceSetting_TagsSetting) erro if metadata == nil { return errors.Errorf("tag metadata is required for %q", tag) } - if metadata.GetBackgroundColor() == nil { - return errors.Errorf("background_color is required for %q", tag) - } - if err := validateInstanceColor(metadata.GetBackgroundColor()); err != nil { - return errors.Wrapf(err, "background_color for %q", tag) + if metadata.GetBackgroundColor() != nil { + if err := validateInstanceColor(metadata.GetBackgroundColor()); err != nil { + return errors.Wrapf(err, "background_color for %q", tag) + } } } return nil diff --git a/server/router/api/v1/test/instance_service_test.go b/server/router/api/v1/test/instance_service_test.go index bd7e3f288..eac97b58e 100644 --- a/server/router/api/v1/test/instance_service_test.go +++ b/server/router/api/v1/test/instance_service_test.go @@ -318,6 +318,34 @@ func TestUpdateInstanceSetting(t *testing.T) { require.Contains(t, err.Error(), "invalid instance setting") }) + t.Run("UpdateInstanceSetting - tags setting without color", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + resp, err := ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{ + Setting: &v1pb.InstanceSetting{ + Name: "instance/settings/TAGS", + Value: &v1pb.InstanceSetting_TagsSetting_{ + TagsSetting: &v1pb.InstanceSetting_TagsSetting{ + Tags: map[string]*v1pb.InstanceSetting_TagMetadata{ + "spoiler": { + BlurContent: true, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, resp.GetTagsSetting()) + require.Contains(t, resp.GetTagsSetting().GetTags(), "spoiler") + require.Nil(t, resp.GetTagsSetting().GetTags()["spoiler"].GetBackgroundColor()) + require.True(t, resp.GetTagsSetting().GetTags()["spoiler"].GetBlurContent()) + }) + t.Run("UpdateInstanceSetting - notification setting password is write-only", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() diff --git a/store/test/instance_setting_test.go b/store/test/instance_setting_test.go index 1451234ec..bf63b1fe3 100644 --- a/store/test/instance_setting_test.go +++ b/store/test/instance_setting_test.go @@ -257,6 +257,34 @@ func TestInstanceSettingTagsSetting(t *testing.T) { ts.Close() } +func TestInstanceSettingTagsSettingWithoutColor(t *testing.T) { + t.Parallel() + ctx := context.Background() + ts := NewTestingStore(ctx, t) + + _, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ + Key: storepb.InstanceSettingKey_TAGS, + Value: &storepb.InstanceSetting_TagsSetting{ + TagsSetting: &storepb.InstanceTagsSetting{ + Tags: map[string]*storepb.InstanceTagMetadata{ + "spoiler": { + BlurContent: true, + }, + }, + }, + }, + }) + require.NoError(t, err) + + tagsSetting, err := ts.GetInstanceTagsSetting(ctx) + require.NoError(t, err) + require.Contains(t, tagsSetting.Tags, "spoiler") + require.Nil(t, tagsSetting.Tags["spoiler"].GetBackgroundColor()) + require.True(t, tagsSetting.Tags["spoiler"].GetBlurContent()) + + ts.Close() +} + func TestInstanceSettingNotificationSetting(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/web/src/components/Settings/TagsSection.tsx b/web/src/components/Settings/TagsSection.tsx index 8c6ef8b68..b1e3aa439 100644 --- a/web/src/components/Settings/TagsSection.tsx +++ b/web/src/components/Settings/TagsSection.tsx @@ -22,8 +22,7 @@ import SettingGroup from "./SettingGroup"; import SettingSection from "./SettingSection"; import SettingTable from "./SettingTable"; -// Fallback to white when no color is stored. -const tagColorToHex = (color?: { red?: number; green?: number; blue?: number }): string => colorToHex(color) ?? "#ffffff"; +const DEFAULT_TAG_COLOR = "#ffffff"; // Converts a CSS hex string to a google.type.Color message. const hexToColor = (hex: string) => @@ -34,10 +33,15 @@ const hexToColor = (hex: string) => }); interface LocalTagMeta { - color: string; + color?: string; blur: boolean; } +const toLocalTagMeta = (meta: { backgroundColor?: { red?: number; green?: number; blue?: number }; blurContent: boolean }): LocalTagMeta => ({ + color: colorToHex(meta.backgroundColor), + blur: meta.blurContent, +}); + const TagsSection = () => { const t = useTranslate(); const { tagsSetting: originalSetting, updateSetting, fetchSetting } = useInstance(); @@ -45,28 +49,16 @@ const TagsSection = () => { // Local state: map of tagName → { color, blur } for editing. const [localTags, setLocalTags] = useState>(() => - Object.fromEntries( - Object.entries(originalSetting.tags).map(([name, meta]) => [ - name, - { color: tagColorToHex(meta.backgroundColor), blur: meta.blurContent }, - ]), - ), + Object.fromEntries(Object.entries(originalSetting.tags).map(([name, meta]) => [name, toLocalTagMeta(meta)])), ); const [newTagName, setNewTagName] = useState(""); - const [newTagColor, setNewTagColor] = useState("#ffffff"); + const [newTagColor, setNewTagColor] = useState(undefined); const [newTagBlur, setNewTagBlur] = useState(false); // Sync local state when the fetched setting arrives (the fetch is async and // completes after mount, so localTags would be empty without this sync). useEffect(() => { - setLocalTags( - Object.fromEntries( - Object.entries(originalSetting.tags).map(([name, meta]) => [ - name, - { color: tagColorToHex(meta.backgroundColor), blur: meta.blurContent }, - ]), - ), - ); + setLocalTags(Object.fromEntries(Object.entries(originalSetting.tags).map(([name, meta]) => [name, toLocalTagMeta(meta)]))); }, [originalSetting.tags]); // All known tag names: union of saved entries and tags used in memos. @@ -85,13 +77,7 @@ const TagsSection = () => { ); const originalMetaMap = useMemo( - () => - Object.fromEntries( - Object.entries(originalSetting.tags).map(([name, meta]) => [ - name, - { color: tagColorToHex(meta.backgroundColor), blur: meta.blurContent }, - ]), - ), + () => Object.fromEntries(Object.entries(originalSetting.tags).map(([name, meta]) => [name, toLocalTagMeta(meta)])), [originalSetting.tags], ); const hasChanges = !isEqual(localTags, originalMetaMap); @@ -104,6 +90,10 @@ const TagsSection = () => { setLocalTags((prev) => ({ ...prev, [tagName]: { ...prev[tagName], blur } })); }; + const handleClearColor = (tagName: string) => { + setLocalTags((prev) => ({ ...prev, [tagName]: { ...prev[tagName], color: undefined } })); + }; + const handleRemoveTag = (tagName: string) => { setLocalTags((prev) => { const next = { ...prev }; @@ -125,7 +115,7 @@ const TagsSection = () => { } setLocalTags((prev) => ({ ...prev, [name]: { color: newTagColor, blur: newTagBlur } })); setNewTagName(""); - setNewTagColor("#ffffff"); + setNewTagColor(undefined); setNewTagBlur(false); }; @@ -134,7 +124,10 @@ const TagsSection = () => { const tags = Object.fromEntries( Object.entries(localTags).map(([name, meta]) => [ name, - create(InstanceSetting_TagMetadataSchema, { backgroundColor: hexToColor(meta.color), blurContent: meta.blur }), + create(InstanceSetting_TagMetadataSchema, { + blurContent: meta.blur, + ...(meta.color ? { backgroundColor: hexToColor(meta.color) } : {}), + }), ]), ); await updateSetting( @@ -171,9 +164,13 @@ const TagsSection = () => { handleColorChange(row.name, e.target.value)} /> + + {!localTags[row.name].color && {t("setting.tags.using-default-color")}} ), }, @@ -224,9 +221,12 @@ const TagsSection = () => { setNewTagColor(e.target.value)} /> +