diff --git a/api/migration/src/lib.rs b/api/migration/src/lib.rs index 23ad27c..fe9e9de 100644 --- a/api/migration/src/lib.rs +++ b/api/migration/src/lib.rs @@ -11,6 +11,7 @@ mod m20260525_164927_add_time_recurring_yearly; mod m20260529_073701_add_delete_time; mod m20260610_061458_add_todo_reminders; mod m20260907_124516_add_organizations; +mod m20260917_155014_add_tag_group; pub struct Migrator; @@ -29,6 +30,7 @@ impl MigratorTrait for Migrator { Box::new(m20260529_073701_add_delete_time::Migration), Box::new(m20260610_061458_add_todo_reminders::Migration), Box::new(m20260907_124516_add_organizations::Migration), + Box::new(m20260917_155014_add_tag_group::Migration), ] } } diff --git a/api/migration/src/m20260917_155014_add_tag_group.rs b/api/migration/src/m20260917_155014_add_tag_group.rs new file mode 100644 index 0000000..aea95dc --- /dev/null +++ b/api/migration/src/m20260917_155014_add_tag_group.rs @@ -0,0 +1,41 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +use crate::m20260503_093223_add_tag_table::Tag; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20260917_155014_add_tag_group" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Tag::Table) + .add_column(integer(TagGroup::Group).default(0)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Tag::Table) + .drop_column(TagGroup::Group) + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum TagGroup { + Group, +} diff --git a/api/orm/src/entities/tag.rs b/api/orm/src/entities/tag.rs index fc56401..c689c29 100644 --- a/api/orm/src/entities/tag.rs +++ b/api/orm/src/entities/tag.rs @@ -12,6 +12,7 @@ pub struct Model { pub color: String, pub owner_id: String, pub deleted: Option, + pub group: i32, #[sea_orm(has_many, via = "todo_tag")] pub todos: HasMany, } diff --git a/api/services/src/tags.rs b/api/services/src/tags.rs index 4ef836f..1984c60 100644 --- a/api/services/src/tags.rs +++ b/api/services/src/tags.rs @@ -70,6 +70,7 @@ impl TagService { owner_id: Set(user.id().to_string()), name: Set(tag.name), color: Set(tag.color.as_str().to_string()), + group: Set(tag.group), deleted: NotSet, }) .exec_with_returning(db) @@ -84,6 +85,7 @@ impl TagService { owner_id: Set(user.id().to_string()), name: Set(tag.name), color: Set(tag.color.as_str().to_string()), + group: Set(tag.group), deleted: NotSet, }) .validate()? @@ -101,6 +103,7 @@ pub fn to_tag_dto(value: &tag::ModelEx) -> Result { name: value.name.clone(), color: HexColor::from_text(&value.color) .ok_or(TagServiceError::InvalidTag("invalid color".to_string()))?, + group: value.group, }; Ok(tag) diff --git a/api/tests/sse.rs b/api/tests/sse.rs index 7f56288..a72b833 100644 --- a/api/tests/sse.rs +++ b/api/tests/sse.rs @@ -320,6 +320,7 @@ async fn sends_events_on_add_tag() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; @@ -343,6 +344,7 @@ async fn sends_events_on_update_tag() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; @@ -354,6 +356,7 @@ async fn sends_events_on_update_tag() { "uuid": tag_uuid, "name": "Updated Tag", "color": "#233212", + "group": 5, })) .await; @@ -377,6 +380,7 @@ async fn sends_events_on_remove_tag() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 5, } )) .await; diff --git a/api/tests/tags/add.rs b/api/tests/tags/add.rs index adcc83c..636d55d 100644 --- a/api/tests/tags/add.rs +++ b/api/tests/tags/add.rs @@ -12,6 +12,7 @@ async fn add_tag() { uuid: tag_uuid, name: "Test Tag".to_string(), color: HexColor::from_text("#233212").unwrap(), + group: -2, }; let response = client @@ -20,6 +21,7 @@ async fn add_tag() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": -2, } )) .await; @@ -45,6 +47,7 @@ async fn add_tag_known_uuid() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; @@ -55,6 +58,7 @@ async fn add_tag_known_uuid() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; @@ -83,6 +87,19 @@ async fn add_invalid_tag() { "uuid": tag_uuid, "name": "Test Tag", "color": "someColor", + "group": 0, + } + )) + .await; + response.assert_status_unprocessable_entity(); + + let response = client + .add_tag(json!( + { + "uuid": tag_uuid, + "name": "Test Tag", + "color": "#233212", + "group": "not a number", } )) .await; diff --git a/api/tests/tags/delete.rs b/api/tests/tags/delete.rs index 11c2e98..227eb0c 100644 --- a/api/tests/tags/delete.rs +++ b/api/tests/tags/delete.rs @@ -14,6 +14,7 @@ async fn delete_tag() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, })) .await; diff --git a/api/tests/tags/update.rs b/api/tests/tags/update.rs index c22b566..2db3cf3 100644 --- a/api/tests/tags/update.rs +++ b/api/tests/tags/update.rs @@ -14,6 +14,7 @@ async fn update_tag() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; @@ -24,6 +25,7 @@ async fn update_tag() { "uuid": tag_uuid, "name": "Updated Tag", "color": "#000000", + "group": 10, } )) .await; @@ -47,6 +49,7 @@ async fn update_tag_unknown() { "uuid": tag_uuid, "name": "Updated Tag", "color": "#000000", + "group": 0, } )) .await; @@ -65,6 +68,7 @@ async fn update_invalid_tag() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; diff --git a/api/tests/todos/add.rs b/api/tests/todos/add.rs index 2642673..2d69f72 100644 --- a/api/tests/todos/add.rs +++ b/api/tests/todos/add.rs @@ -430,6 +430,7 @@ async fn add_todo_with_labels() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; @@ -443,6 +444,7 @@ async fn add_todo_with_labels() { uuid: tag_uuid, name: "Test Tag".to_string(), color: HexColor::from_text("#233212").unwrap(), + group: 0, }]), category: Some(Category { uuid: category_uuid, diff --git a/api/tests/todos/delete.rs b/api/tests/todos/delete.rs index 4711cd2..7adf238 100644 --- a/api/tests/todos/delete.rs +++ b/api/tests/todos/delete.rs @@ -134,6 +134,7 @@ async fn get_deleted_todo_with_labels() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; @@ -147,6 +148,7 @@ async fn get_deleted_todo_with_labels() { uuid: tag_uuid, name: "Test Tag".to_string(), color: HexColor::from_text("#233212").unwrap(), + group: 0, }]), category: Some(Category { uuid: category_uuid, @@ -205,6 +207,7 @@ async fn reactivate_deleted_todo_recover_labels() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0 } )) .await; @@ -218,6 +221,7 @@ async fn reactivate_deleted_todo_recover_labels() { uuid: tag_uuid, name: "Test Tag".to_string(), color: HexColor::from_text("#233212").unwrap(), + group: 0, }]), category: Some(Category { uuid: category_uuid, diff --git a/api/tests/todos/update.rs b/api/tests/todos/update.rs index f04954a..cefa01c 100644 --- a/api/tests/todos/update.rs +++ b/api/tests/todos/update.rs @@ -159,6 +159,7 @@ async fn update_todo_with_labels() { "uuid": tag_uuid, "name": "Test Tag", "color": "#233212", + "group": 0, } )) .await; @@ -198,6 +199,7 @@ async fn update_todo_with_labels() { uuid: tag_uuid, name: "Test Tag".to_string(), color: HexColor::from_text("#233212").unwrap(), + group: 0 } ); diff --git a/app/components/Edit/DesktopTodoDataSelect.vue b/app/components/Edit/DesktopTodoDataSelect.vue index 5bf6cbd..6fbf386 100644 --- a/app/components/Edit/DesktopTodoDataSelect.vue +++ b/app/components/Edit/DesktopTodoDataSelect.vue @@ -5,10 +5,10 @@ import type { Category as CategoryType, CreateTodoData, - Label as LabelType, + Tag as TagType, } from "~/utils/types"; import AddCategory from "../Settings/Labels/AddCategory.vue"; - import AddLabel from "../Settings/Labels/AddLabel.vue"; + import AddTag from "../Settings/Labels/AddTag.vue"; import CategorySelect from "../Utils/Label/CategorySelect.vue"; import TagSelect from "../Utils/Label/TagSelect.vue"; import SegmentedRadioGroup from "../Utils/SegmentedRadioGroup.vue"; @@ -46,8 +46,8 @@ }); const { mutate: addTag } = useAddTag(); - function onAddTag(tag: LabelType) { - addTag({ name: tag.name, color: tag.color }); + function onAddTag(tag: TagType) { + addTag({ name: tag.name, color: tag.color, group: tag.group }); } const { mutate: addCategory } = useAddCategory(); @@ -89,7 +89,7 @@ - import type { Label } from "~/utils/types"; - import EditLabel from "./EditLabel.vue"; + import type { Tag } from "~/utils/types"; + import EditTag from "./EditTag.vue"; - const emit = defineEmits<{ add: [Label] }>(); + const emit = defineEmits<{ add: [Tag] }>(); - function onAddTag(label: Label) { + function onAddTag(label: Tag) { emit("add", label); } diff --git a/app/components/Settings/Labels/EditTag.vue b/app/components/Settings/Labels/EditTag.vue new file mode 100644 index 0000000..a25e2cf --- /dev/null +++ b/app/components/Settings/Labels/EditTag.vue @@ -0,0 +1,43 @@ + + diff --git a/app/components/Settings/Labels/Tag.vue b/app/components/Settings/Labels/Tag.vue index dc55e73..826866d 100644 --- a/app/components/Settings/Labels/Tag.vue +++ b/app/components/Settings/Labels/Tag.vue @@ -1,15 +1,15 @@ diff --git a/app/components/Settings/Tags.vue b/app/components/Settings/Tags.vue index b522ead..2d74ece 100644 --- a/app/components/Settings/Tags.vue +++ b/app/components/Settings/Tags.vue @@ -1,9 +1,9 @@ @@ -46,7 +46,7 @@ @delete="onDeleteTag" /> - + diff --git a/app/mutations/tags.ts b/app/mutations/tags.ts index e38815f..c4e268e 100644 --- a/app/mutations/tags.ts +++ b/app/mutations/tags.ts @@ -29,7 +29,7 @@ export const useDeleteTag = defineMutation({ export const useAddTag = defineMutation({ async mutation( - label: { name: string; color: string }, + label: { name: string; color: string; group: number }, { uuid }: { uuid: UUID }, ) { const { $api } = useNuxtApp(); @@ -38,7 +38,15 @@ export const useAddTag = defineMutation({ body: { ...label, uuid }, }); }, - onMutate({ name, color }: { name: string; color: string }) { + onMutate({ + name, + color, + group, + }: { + name: string; + color: string; + group: number; + }) { const queryCache = useQueryCache(); const activeTags = queryCache.getQueryData(["tags"]); @@ -48,6 +56,7 @@ export const useAddTag = defineMutation({ uuid: v4(), name, color, + group, }; activeTags.push(label); @@ -65,14 +74,19 @@ export const useAddTag = defineMutation({ }); export const useUpdateTag = defineMutation({ - async mutation(label: { uuid: UUID; name: string; color: string }) { + async mutation(label: { + uuid: UUID; + name: string; + color: string; + group: number; + }) { const { $api } = useNuxtApp(); return await $api(`/api/tags`, { method: "PUT", body: label, }); }, - onMutate(label: { uuid: UUID; name: string; color: string }) { + onMutate(label: { uuid: UUID; name: string; color: string; group: number }) { const queryCache = useQueryCache(); const activeTags = queryCache.getQueryData(["tags"]); diff --git a/app/queries/tag.ts b/app/queries/tag.ts index ea1eaa4..ff385f3 100644 --- a/app/queries/tag.ts +++ b/app/queries/tag.ts @@ -2,7 +2,7 @@ export const useTagsQuery = defineQueryOptions(() => ({ key: ["tags"], query: () => { const { $api } = useNuxtApp(); - return $api("/api/tags"); + return $api("/api/tags"); }, ssrCatchError: true, })); diff --git a/app/utils/types.ts b/app/utils/types.ts index 165f9b7..fcc5245 100644 --- a/app/utils/types.ts +++ b/app/utils/types.ts @@ -55,7 +55,7 @@ export type Todo = { title: string; note: string; time?: Time; - tags?: Label[]; + tags?: Tag[]; category?: Category; checks?: string[]; // ISO dates reminders?: Reminder[]; @@ -71,6 +71,10 @@ export type Category = Label & { icon: string; }; +export type Tag = Label & { + group: number; +}; + export type PushSubscriptionJSON = { endpoint: string; expirationTime?: number | null; diff --git a/libs/types/src/tag.rs b/libs/types/src/tag.rs index 11646a9..d785f0c 100644 --- a/libs/types/src/tag.rs +++ b/libs/types/src/tag.rs @@ -9,6 +9,7 @@ pub struct Tag { pub uuid: Uuid, pub name: String, pub color: HexColor, + pub group: i32, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]