mirror of https://github.com/usememos/memos.git
fix: allow tags metadata without background color
This commit is contained in:
parent
201c8a8ea9
commit
cc71d7f113
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<Record<string, LocalTagMeta>>(() =>
|
||||
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<string | undefined>(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 = () => {
|
|||
<input
|
||||
type="color"
|
||||
className="w-8 h-8 cursor-pointer rounded border border-border bg-transparent p-0.5"
|
||||
value={localTags[row.name].color}
|
||||
value={localTags[row.name].color ?? DEFAULT_TAG_COLOR}
|
||||
onChange={(e) => handleColorChange(row.name, e.target.value)}
|
||||
/>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleClearColor(row.name)} disabled={!localTags[row.name].color}>
|
||||
{t("common.clear")}
|
||||
</Button>
|
||||
{!localTags[row.name].color && <span className="text-xs text-muted-foreground">{t("setting.tags.using-default-color")}</span>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -224,9 +221,12 @@ const TagsSection = () => {
|
|||
<input
|
||||
type="color"
|
||||
className="w-8 h-8 cursor-pointer rounded border border-border bg-transparent p-0.5"
|
||||
value={newTagColor}
|
||||
value={newTagColor ?? DEFAULT_TAG_COLOR}
|
||||
onChange={(e) => setNewTagColor(e.target.value)}
|
||||
/>
|
||||
<Button variant="ghost" size="sm" onClick={() => setNewTagColor(undefined)} disabled={!newTagColor}>
|
||||
{t("common.clear")}
|
||||
</Button>
|
||||
<label className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -242,6 +242,7 @@ const TagsSection = () => {
|
|||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t("setting.tags.tag-pattern-hint")}</p>
|
||||
{!newTagColor && <p className="text-xs text-muted-foreground">{t("setting.tags.using-default-color")}</p>}
|
||||
</SettingGroup>
|
||||
|
||||
<div className="w-full flex justify-end">
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@
|
|||
"tags": {
|
||||
"label": "Tags",
|
||||
"title": "Tag metadata",
|
||||
"description": "Assign display colors to tags instance-wide. Tag names are treated as anchored regex patterns.",
|
||||
"description": "Assign optional display colors to tags instance-wide, or blur matching memo content. Tag names are treated as anchored regex patterns.",
|
||||
"background-color": "Background color",
|
||||
"blur-content": "Blur content",
|
||||
"no-tags-configured": "No tag metadata configured.",
|
||||
|
|
@ -483,7 +483,8 @@
|
|||
"tag-name-placeholder": "e.g. work or project/.*",
|
||||
"tag-already-exists": "Tag already exists.",
|
||||
"tag-pattern-hint": "Tag name or regex pattern (e.g. project/.* matches all project/ tags)",
|
||||
"invalid-regex": "Invalid or unsafe regex pattern."
|
||||
"invalid-regex": "Invalid or unsafe regex pattern.",
|
||||
"using-default-color": "Using default color."
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
|
|
|
|||
|
|
@ -414,7 +414,8 @@ export const InstanceSetting_MemoRelatedSettingSchema: GenMessage<InstanceSettin
|
|||
*/
|
||||
export type InstanceSetting_TagMetadata = Message<"memos.api.v1.InstanceSetting.TagMetadata"> & {
|
||||
/**
|
||||
* Background color for the tag label.
|
||||
* Optional background color for the tag label.
|
||||
* When unset, the default tag color is used.
|
||||
*
|
||||
* @generated from field: google.type.Color background_color = 1;
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue