diff --git a/src/backend/core/api/openapi.json b/src/backend/core/api/openapi.json
index 66bf23d3..7de90aaf 100644
--- a/src/backend/core/api/openapi.json
+++ b/src/backend/core/api/openapi.json
@@ -4710,7 +4710,10 @@
"readOnly": true
},
"labels": {
- "type": "string",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ThreadLabel"
+ },
"readOnly": true
}
},
@@ -4833,6 +4836,44 @@
"type": "string",
"description": "* `viewer` - Viewer\n* `editor` - Editor"
},
+ "ThreadLabel": {
+ "type": "object",
+ "description": "Serializer to get labels details for a thread.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid",
+ "readOnly": true,
+ "description": "primary key for the record as UUID"
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')",
+ "maxLength": 255
+ },
+ "slug": {
+ "type": "string",
+ "readOnly": true,
+ "description": "URL-friendly version of the name",
+ "pattern": "^[-a-zA-Z0-9_]+$"
+ },
+ "color": {
+ "type": "string",
+ "description": "Color of the label in hex format (e.g. #FF0000)",
+ "maxLength": 7
+ },
+ "display_name": {
+ "type": "string",
+ "readOnly": true
+ }
+ },
+ "required": [
+ "display_name",
+ "id",
+ "name",
+ "slug"
+ ]
+ },
"User": {
"type": "object",
"description": "Serialize users.",
diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py
index 0e3a6372..05d54476 100644
--- a/src/backend/core/api/serializers.py
+++ b/src/backend/core/api/serializers.py
@@ -146,6 +146,51 @@ class AttachmentSerializer(serializers.ModelSerializer):
]
read_only_fields = fields
+class ThreadLabelSerializer(serializers.ModelSerializer):
+ """Serializer to get labels details for a thread."""
+
+ display_name = serializers.SerializerMethodField(read_only=True)
+
+ class Meta:
+ model = models.Label
+ fields = ["id", "name", "slug", "color", "display_name"]
+ read_only_fields = ["id", "slug", "display_name"]
+
+ def get_display_name(self, instance):
+ """Return the display name of the label."""
+ return instance.name.split("/")[-1]
+
+class LabelSerializer(serializers.ModelSerializer):
+ """Serializer for Label model."""
+
+ class Meta:
+ model = models.Label
+ fields = ["id", "name", "slug", "color", "mailbox", "threads"]
+ read_only_fields = ["id", "slug"]
+
+ def validate(self, attrs):
+ """Validate that the label name is unique within the mailbox."""
+ if models.Label.objects.filter(
+ name=attrs.get("name"), mailbox=attrs.get("mailbox")
+ ).exists():
+ raise serializers.ValidationError(
+ {"name": "A label with this name already exists in this mailbox."}
+ )
+ return attrs
+
+ def validate_mailbox(self, value):
+ """Validate that user has access to the mailbox."""
+ user = self.context["request"].user
+ if not value.accesses.filter(
+ user=user,
+ role__in=[
+ models.MailboxRoleChoices.ADMIN,
+ models.MailboxRoleChoices.EDITOR,
+ ],
+ ).exists():
+ raise PermissionDenied("You don't have access to this mailbox")
+ return value
+
class ThreadAccessDetailSerializer(serializers.ModelSerializer):
"""Serializer for thread access details."""
@@ -196,6 +241,7 @@ class ThreadSerializer(serializers.ModelSerializer):
return None
return None
+ @extend_schema_field(ThreadLabelSerializer(many=True))
def get_labels(self, instance):
"""Get labels for the thread, filtered by user's mailbox access."""
request = self.context.get("request")
@@ -549,49 +595,3 @@ class ImportIMAPSerializer(ImportBaseSerializer):
default=0,
min_value=0,
)
-
-
-class ThreadLabelSerializer(serializers.ModelSerializer):
- """Serializer to get labels details for a thread."""
-
- display_name = serializers.SerializerMethodField(read_only=True)
-
- class Meta:
- model = models.Label
- fields = ["id", "name", "slug", "color", "display_name"]
- read_only_fields = ["id", "slug", "display_name"]
-
- def get_display_name(self, instance):
- """Return the display name of the label."""
- return instance.name.split("/")[-1]
-
-class LabelSerializer(serializers.ModelSerializer):
- """Serializer for Label model."""
-
- class Meta:
- model = models.Label
- fields = ["id", "name", "slug", "color", "mailbox", "threads"]
- read_only_fields = ["id", "slug"]
-
- def validate(self, attrs):
- """Validate that the label name is unique within the mailbox."""
- if models.Label.objects.filter(
- name=attrs.get("name"), mailbox=attrs.get("mailbox")
- ).exists():
- raise serializers.ValidationError(
- {"name": "A label with this name already exists in this mailbox."}
- )
- return attrs
-
- def validate_mailbox(self, value):
- """Validate that user has access to the mailbox."""
- user = self.context["request"].user
- if not value.accesses.filter(
- user=user,
- role__in=[
- models.MailboxRoleChoices.ADMIN,
- models.MailboxRoleChoices.EDITOR,
- ],
- ).exists():
- raise PermissionDenied("You don't have access to this mailbox")
- return value
diff --git a/src/frontend/src/features/api/gen/models/index.ts b/src/frontend/src/features/api/gen/models/index.ts
index eb72b057..14c31785 100644
--- a/src/frontend/src/features/api/gen/models/index.ts
+++ b/src/frontend/src/features/api/gen/models/index.ts
@@ -71,6 +71,7 @@ export * from "./thread_access";
export * from "./thread_access_detail";
export * from "./thread_access_request";
export * from "./thread_access_role_enum";
+export * from "./thread_label";
export * from "./threads_accesses_create_params";
export * from "./threads_accesses_destroy_params";
export * from "./threads_accesses_list_params";
diff --git a/src/frontend/src/features/api/gen/models/thread.ts b/src/frontend/src/features/api/gen/models/thread.ts
index 95cac6f9..aa53a99c 100644
--- a/src/frontend/src/features/api/gen/models/thread.ts
+++ b/src/frontend/src/features/api/gen/models/thread.ts
@@ -6,7 +6,7 @@
* OpenAPI spec version: 1.0.0 (v1.0)
*/
import type { ThreadAccessDetail } from "./thread_access_detail";
-import type { Label } from "./label";
+import type { ThreadLabel } from "./thread_label";
/**
* Serialize threads.
@@ -30,6 +30,5 @@ export interface Thread {
readonly updated_at: string;
readonly user_role: string;
readonly accesses: readonly ThreadAccessDetail[];
-
- readonly labels: readonly Label[];
+ readonly labels: readonly ThreadLabel[];
}
diff --git a/src/frontend/src/features/api/gen/models/thread_label.ts b/src/frontend/src/features/api/gen/models/thread_label.ts
new file mode 100644
index 00000000..a88c52d4
--- /dev/null
+++ b/src/frontend/src/features/api/gen/models/thread_label.ts
@@ -0,0 +1,31 @@
+/**
+ * Generated by orval v7.9.0 🍺
+ * Do not edit manually.
+ * messages API
+ * This is the messages API schema.
+ * OpenAPI spec version: 1.0.0 (v1.0)
+ */
+
+/**
+ * Serializer to get labels details for a thread.
+ */
+export interface ThreadLabel {
+ /** primary key for the record as UUID */
+ readonly id: string;
+ /**
+ * Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')
+ * @maxLength 255
+ */
+ name: string;
+ /**
+ * URL-friendly version of the name
+ * @pattern ^[-a-zA-Z0-9_]+$
+ */
+ readonly slug: string;
+ /**
+ * Color of the label in hex format (e.g. #FF0000)
+ * @maxLength 7
+ */
+ color?: string;
+ readonly display_name: string;
+}
diff --git a/src/frontend/src/features/layouts/components/label-badge/index.tsx b/src/frontend/src/features/layouts/components/label-badge/index.tsx
deleted file mode 100644
index eb7be102..00000000
--- a/src/frontend/src/features/layouts/components/label-badge/index.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Badge } from "@/features/ui/components/badge"
-import { Label } from "@/features/api/gen/models/label"
-
-type LabelBadgeProps = {
- label: Label
- size?: "small" | "medium"
-}
-
-export const LabelBadge = ({ label, size = "medium" }: LabelBadgeProps) => {
- return (
-
{thread.subject}
{thread.subject}