mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-17 21:25:41 +02:00
✨(integrations) add integrations view in mailbox (#488)
For now this allows users to create feedback widgets linked to their mailbox. We will also have API keys and recurring imports there.
This commit is contained in:
@@ -286,6 +286,7 @@ _Those settings are deprecated and will be removed in the future._
|
||||
| `AI_MODEL` | None | Default model used for AI features | Optional |
|
||||
| `FEATURE_AI_SUMMARY` | `False` | Default enabled mode for summary AI features | Required |
|
||||
| `FEATURE_AI_AUTOLABELS` | `False` | Default enabled mode for label AI features | Required |
|
||||
| `FEATURE_MAILBOX_ADMIN_CHANNELS` | `` | Comma-separated list of channel types enabled for mailbox admin (e.g., `widget,api_key`). Empty list disables all channel types. | Optional |
|
||||
|
||||
### Image Proxy
|
||||
|
||||
|
||||
@@ -171,6 +171,13 @@
|
||||
"type": "boolean",
|
||||
"readOnly": true
|
||||
},
|
||||
"FEATURE_MAILBOX_ADMIN_CHANNELS": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"readOnly": true
|
||||
},
|
||||
"DRIVE": {
|
||||
"type": "object",
|
||||
"description": "The URLs of the Drive external service.",
|
||||
@@ -241,6 +248,7 @@
|
||||
"AI_ENABLED",
|
||||
"FEATURE_AI_SUMMARY",
|
||||
"FEATURE_AI_AUTOLABELS",
|
||||
"FEATURE_MAILBOX_ADMIN_CHANNELS",
|
||||
"SCHEMA_CUSTOM_ATTRIBUTES_USER",
|
||||
"SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN",
|
||||
"MAX_OUTGOING_ATTACHMENT_SIZE",
|
||||
@@ -2424,6 +2432,313 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1.0/mailboxes/{mailbox_id}/channels/": {
|
||||
"get": {
|
||||
"operationId": "mailboxes_channels_list",
|
||||
"description": "Manage integration channels for a mailbox",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "mailbox_id",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"channels"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "mailboxes_channels_create",
|
||||
"description": "Manage integration channels for a mailbox",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "mailbox_id",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"channels"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChannelRequest"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChannelRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Channel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Channel created successfully"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input data"
|
||||
},
|
||||
"403": {
|
||||
"description": "Permission denied"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1.0/mailboxes/{mailbox_id}/channels/{id}/": {
|
||||
"get": {
|
||||
"operationId": "mailboxes_channels_retrieve",
|
||||
"description": "Manage integration channels for a mailbox",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "mailbox_id",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"channels"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Channel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"operationId": "mailboxes_channels_update",
|
||||
"description": "Manage integration channels for a mailbox",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "mailbox_id",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"channels"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChannelRequest"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChannelRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Channel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Channel updated successfully"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input data"
|
||||
},
|
||||
"403": {
|
||||
"description": "Permission denied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Channel not found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "mailboxes_channels_partial_update",
|
||||
"description": "Manage integration channels for a mailbox",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "mailbox_id",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"channels"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PatchedChannelRequest"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PatchedChannelRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Channel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "mailboxes_channels_destroy",
|
||||
"description": "Manage integration channels for a mailbox",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "mailbox_id",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"channels"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Channel deleted successfully"
|
||||
},
|
||||
"403": {
|
||||
"description": "Permission denied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Channel not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1.0/mailboxes/{mailbox_id}/image-proxy/": {
|
||||
"get": {
|
||||
"operationId": "mailboxes_image_proxy_list",
|
||||
@@ -5218,6 +5533,91 @@
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"Channel": {
|
||||
"type": "object",
|
||||
"description": "Serialize Channel model.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"readOnly": true,
|
||||
"description": "primary key for the record as UUID"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable name for this channel",
|
||||
"maxLength": 255
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Type of channel",
|
||||
"maxLength": 255
|
||||
},
|
||||
"settings": {
|
||||
"description": "Channel-specific configuration settings"
|
||||
},
|
||||
"mailbox": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "primary key for the record as UUID",
|
||||
"readOnly": true,
|
||||
"nullable": true
|
||||
},
|
||||
"maildomain": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "primary key for the record as UUID",
|
||||
"readOnly": true,
|
||||
"nullable": true
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"readOnly": true,
|
||||
"title": "Created on",
|
||||
"description": "date and time at which a record was created"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"readOnly": true,
|
||||
"title": "Updated on",
|
||||
"description": "date and time at which a record was last updated"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"created_at",
|
||||
"id",
|
||||
"mailbox",
|
||||
"maildomain",
|
||||
"name",
|
||||
"updated_at"
|
||||
]
|
||||
},
|
||||
"ChannelRequest": {
|
||||
"type": "object",
|
||||
"description": "Serialize Channel model.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Human-readable name for this channel",
|
||||
"maxLength": 255
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Type of channel",
|
||||
"maxLength": 255
|
||||
},
|
||||
"settings": {
|
||||
"description": "Channel-specific configuration settings"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"Contact": {
|
||||
"type": "object",
|
||||
"description": "Serialize contacts.",
|
||||
@@ -6920,6 +7320,27 @@
|
||||
"size"
|
||||
]
|
||||
},
|
||||
"PatchedChannelRequest": {
|
||||
"type": "object",
|
||||
"description": "Serialize Channel model.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Human-readable name for this channel",
|
||||
"maxLength": 255
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Type of channel",
|
||||
"maxLength": 255
|
||||
},
|
||||
"settings": {
|
||||
"description": "Channel-specific configuration settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PatchedLabelRequest": {
|
||||
"type": "object",
|
||||
"description": "Serializer for Label model.",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Exists, OuterRef, Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -1319,9 +1321,13 @@ class ImportIMAPSerializer(ImportBaseSerializer):
|
||||
)
|
||||
|
||||
|
||||
class ChannelSerializer(AbilitiesModelSerializer):
|
||||
class ChannelSerializer(serializers.ModelSerializer):
|
||||
"""Serialize Channel model."""
|
||||
|
||||
# Explicitly mark nullable fields to fix OpenAPI schema
|
||||
mailbox = serializers.PrimaryKeyRelatedField(read_only=True, allow_null=True)
|
||||
maildomain = serializers.PrimaryKeyRelatedField(read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Channel
|
||||
fields = [
|
||||
@@ -1334,10 +1340,77 @@ class ChannelSerializer(AbilitiesModelSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
read_only_fields = ["id", "mailbox", "maildomain", "created_at", "updated_at"]
|
||||
|
||||
def validate_settings(self, value):
|
||||
"""Validate settings, including tags if present."""
|
||||
if not value:
|
||||
return value
|
||||
|
||||
tags = value.get("tags", [])
|
||||
if not tags:
|
||||
return value
|
||||
|
||||
# Get mailbox from context or instance
|
||||
mailbox = self.context.get("mailbox")
|
||||
if not mailbox and self.instance:
|
||||
mailbox = self.instance.mailbox
|
||||
|
||||
if not mailbox:
|
||||
# Tags require a mailbox - can't use tags without one
|
||||
raise serializers.ValidationError(
|
||||
{"tags": "Tags can only be used when a mailbox is configured."}
|
||||
)
|
||||
|
||||
# Validate each tag
|
||||
invalid_tags = []
|
||||
missing_tags = []
|
||||
|
||||
for tag_id in tags:
|
||||
# Validate UUID format
|
||||
try:
|
||||
tag_uuid = uuid.UUID(str(tag_id))
|
||||
except (ValueError, TypeError):
|
||||
invalid_tags.append(tag_id)
|
||||
continue
|
||||
|
||||
# Check if label exists in the mailbox
|
||||
if not models.Label.objects.filter(id=tag_uuid, mailbox=mailbox).exists():
|
||||
missing_tags.append(tag_id)
|
||||
|
||||
errors = []
|
||||
if invalid_tags:
|
||||
errors.append(f"Invalid tag IDs (not valid UUIDs): {invalid_tags}")
|
||||
if missing_tags:
|
||||
errors.append(f"Tags not found in mailbox: {missing_tags}")
|
||||
|
||||
if errors:
|
||||
raise serializers.ValidationError({"tags": errors})
|
||||
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Validate channel data."""
|
||||
"""Validate channel data.
|
||||
|
||||
When used in the nested mailbox context (via ChannelViewSet),
|
||||
the mailbox is set from context and doesn't need to be validated here.
|
||||
"""
|
||||
# If we have a mailbox in context (from ChannelViewSet), validate channel type
|
||||
# and skip mailbox/maildomain validation.
|
||||
# This allows Django admin to create any channel type.
|
||||
if self.context.get("mailbox"):
|
||||
channel_type = attrs.get("type")
|
||||
if channel_type:
|
||||
allowed_types = settings.FEATURE_MAILBOX_ADMIN_CHANNELS
|
||||
if channel_type not in allowed_types:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"type": f"Channel type '{channel_type}' is not authorized. "
|
||||
f"Allowed types: {', '.join(allowed_types)}"
|
||||
}
|
||||
)
|
||||
return attrs
|
||||
|
||||
mailbox = attrs.get("mailbox")
|
||||
maildomain = attrs.get("maildomain")
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""API ViewSet for Channel model."""
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiResponse,
|
||||
extend_schema,
|
||||
)
|
||||
from rest_framework import mixins, status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from core import models
|
||||
|
||||
from .. import permissions, serializers
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=["channels"], description="Manage integration channels for a mailbox"
|
||||
)
|
||||
class ChannelViewSet(
|
||||
viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
):
|
||||
"""ViewSet for Channel model - allows mailbox admins to manage integration channels."""
|
||||
|
||||
serializer_class = serializers.ChannelSerializer
|
||||
permission_classes = [permissions.IsMailboxAdmin]
|
||||
pagination_class = None
|
||||
lookup_field = "pk"
|
||||
|
||||
@cached_property
|
||||
def mailbox(self):
|
||||
"""Get mailbox from URL parameter."""
|
||||
return get_object_or_404(models.Mailbox, id=self.kwargs["mailbox_id"])
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get channels for the mailbox the user has admin access to."""
|
||||
return models.Channel.objects.filter(mailbox=self.mailbox).order_by(
|
||||
"-created_at"
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Add mailbox to serializer context."""
|
||||
context = super().get_serializer_context()
|
||||
context["mailbox"] = self.mailbox
|
||||
return context
|
||||
|
||||
@extend_schema(
|
||||
request=serializers.ChannelSerializer,
|
||||
responses={
|
||||
201: OpenApiResponse(
|
||||
response=serializers.ChannelSerializer,
|
||||
description="Channel created successfully",
|
||||
),
|
||||
400: OpenApiResponse(description="Invalid input data"),
|
||||
403: OpenApiResponse(description="Permission denied"),
|
||||
},
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Create a new channel for the mailbox."""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(mailbox=self.mailbox)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@extend_schema(
|
||||
request=serializers.ChannelSerializer,
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
response=serializers.ChannelSerializer,
|
||||
description="Channel updated successfully",
|
||||
),
|
||||
400: OpenApiResponse(description="Invalid input data"),
|
||||
403: OpenApiResponse(description="Permission denied"),
|
||||
404: OpenApiResponse(description="Channel not found"),
|
||||
},
|
||||
)
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""Update a channel."""
|
||||
partial = kwargs.pop("partial", False)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
204: OpenApiResponse(description="Channel deleted successfully"),
|
||||
403: OpenApiResponse(description="Permission denied"),
|
||||
404: OpenApiResponse(description="Channel not found"),
|
||||
},
|
||||
)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Delete a channel."""
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -38,6 +38,11 @@ class ConfigView(drf.views.APIView):
|
||||
"type": "boolean",
|
||||
"readOnly": True,
|
||||
},
|
||||
"FEATURE_MAILBOX_ADMIN_CHANNELS": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"readOnly": True,
|
||||
},
|
||||
"DRIVE": {
|
||||
"type": "object",
|
||||
"description": "The URLs of the Drive external service.",
|
||||
@@ -109,6 +114,7 @@ class ConfigView(drf.views.APIView):
|
||||
"AI_ENABLED",
|
||||
"FEATURE_AI_SUMMARY",
|
||||
"FEATURE_AI_AUTOLABELS",
|
||||
"FEATURE_MAILBOX_ADMIN_CHANNELS",
|
||||
"SCHEMA_CUSTOM_ATTRIBUTES_USER",
|
||||
"SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN",
|
||||
"MAX_OUTGOING_ATTACHMENT_SIZE",
|
||||
@@ -145,6 +151,11 @@ class ConfigView(drf.views.APIView):
|
||||
dict_settings["FEATURE_AI_SUMMARY"] = is_ai_summary_enabled()
|
||||
dict_settings["FEATURE_AI_AUTOLABELS"] = is_auto_labels_enabled()
|
||||
|
||||
# Feature flags - return as list
|
||||
dict_settings["FEATURE_MAILBOX_ADMIN_CHANNELS"] = list(
|
||||
settings.FEATURE_MAILBOX_ADMIN_CHANNELS
|
||||
)
|
||||
|
||||
# Email size limits
|
||||
dict_settings["MAX_OUTGOING_ATTACHMENT_SIZE"] = (
|
||||
settings.MAX_OUTGOING_ATTACHMENT_SIZE
|
||||
|
||||
@@ -144,10 +144,23 @@ class InboundWidgetViewSet(viewsets.GenericViewSet):
|
||||
),
|
||||
)
|
||||
|
||||
# Build subject from template or use default
|
||||
# Template can use {referer_domain} placeholder (same format as signature templates)
|
||||
default_subject_template = "Message from {referer_domain}"
|
||||
subject_template = (channel.settings or {}).get(
|
||||
"subject_template", default_subject_template
|
||||
)
|
||||
|
||||
# Replace template variables
|
||||
subject = subject_template.replace("{referer_domain}", source_name)
|
||||
|
||||
# Sanitize subject to prevent header injection (strip newlines/carriage returns)
|
||||
subject = subject.replace("\r", "").replace("\n", "")
|
||||
|
||||
# Build a JMAP-like structured format that we could have got from parse_email_message()
|
||||
|
||||
parsed_email = {
|
||||
"subject": f"Message from {source_name}",
|
||||
"subject": subject,
|
||||
"from": {"email": sender_email},
|
||||
"to": [{"name": target_name, "email": target_email}],
|
||||
"date": timezone.now(),
|
||||
|
||||
@@ -249,6 +249,20 @@ def _create_message_from_inbound(
|
||||
logger.exception("Error creating label %s: %s", label, e)
|
||||
continue
|
||||
|
||||
# Apply labels from channel settings (e.g., widget channel tags)
|
||||
if channel and channel.settings:
|
||||
channel_tags = channel.settings.get("tags", [])
|
||||
for tag_id in channel_tags:
|
||||
try:
|
||||
label_obj = models.Label.objects.get(id=tag_id, mailbox=mailbox)
|
||||
thread.labels.add(label_obj)
|
||||
except models.Label.DoesNotExist:
|
||||
logger.warning(
|
||||
"Label %s not found for channel %s, skipping", tag_id, channel.id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Error adding label %s from channel: %s", tag_id, e)
|
||||
|
||||
# --- 4. Get or Create Sender Contact --- #
|
||||
sender_info = parsed_email.get("from", {})
|
||||
sender_email = sender_info.get("email")
|
||||
@@ -502,8 +516,11 @@ def _create_message_from_inbound(
|
||||
thread.summary = new_summary
|
||||
thread.save(update_fields=["summary"])
|
||||
|
||||
# Assign labels to the thread
|
||||
if is_auto_labels_enabled():
|
||||
# Assign labels to the thread (skip if channel already applied tags)
|
||||
has_channel_tags = (
|
||||
channel and channel.settings and channel.settings.get("tags")
|
||||
)
|
||||
if is_auto_labels_enabled() and not has_channel_tags:
|
||||
assign_label_to_thread(thread, mailbox.id)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
"""Tests for the channel API endpoints."""
|
||||
|
||||
# pylint: disable=redefined-outer-name, unused-argument, too-many-public-methods
|
||||
|
||||
import uuid
|
||||
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import models
|
||||
from core.factories import (
|
||||
ChannelFactory,
|
||||
LabelFactory,
|
||||
MailboxFactory,
|
||||
MailDomainAccessFactory,
|
||||
MailDomainFactory,
|
||||
UserFactory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
"""Create a test user."""
|
||||
return UserFactory()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mailbox(user):
|
||||
"""Create a test mailbox with admin access for the user."""
|
||||
mailbox = MailboxFactory()
|
||||
mailbox.accesses.create(user=user, role=models.MailboxRoleChoices.ADMIN)
|
||||
return mailbox
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_client(user):
|
||||
"""Create an authenticated API client."""
|
||||
client = APIClient()
|
||||
client.force_authenticate(user=user)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def channel(mailbox):
|
||||
"""Create a test channel."""
|
||||
return ChannelFactory(mailbox=mailbox, type="widget")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestChannelList:
|
||||
"""Test the channel list endpoint."""
|
||||
|
||||
def test_list_channels(self, api_client, mailbox, channel):
|
||||
"""Test listing channels for a mailbox."""
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data) == 1
|
||||
assert response.data[0]["id"] == str(channel.id)
|
||||
assert response.data[0]["name"] == channel.name
|
||||
assert response.data[0]["type"] == "widget"
|
||||
|
||||
def test_list_channels_empty(self, api_client, mailbox):
|
||||
"""Test listing channels when none exist."""
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data) == 0
|
||||
|
||||
def test_list_channels_no_access(self, api_client):
|
||||
"""Test listing channels for a mailbox the user has no access to."""
|
||||
other_mailbox = MailboxFactory()
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": other_mailbox.id})
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_list_channels_viewer_access(self, api_client, user):
|
||||
"""Test listing channels with viewer role (should fail - admin required)."""
|
||||
mailbox = MailboxFactory()
|
||||
mailbox.accesses.create(user=user, role=models.MailboxRoleChoices.VIEWER)
|
||||
ChannelFactory(mailbox=mailbox)
|
||||
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestChannelCreate:
|
||||
"""Test the channel creation endpoint."""
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_create_widget_channel(self, api_client, mailbox):
|
||||
"""Test creating a widget channel."""
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {
|
||||
"name": "My Widget",
|
||||
"type": "widget",
|
||||
"settings": {
|
||||
"subject_template": "New inquiry from {referer_domain}",
|
||||
"config": {"enabled": True},
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["name"] == "My Widget"
|
||||
assert response.data["type"] == "widget"
|
||||
assert (
|
||||
response.data["settings"]["subject_template"]
|
||||
== "New inquiry from {referer_domain}"
|
||||
)
|
||||
assert str(response.data["mailbox"]) == str(mailbox.id)
|
||||
|
||||
# Verify in database
|
||||
channel = models.Channel.objects.get(id=response.data["id"])
|
||||
assert channel.mailbox == mailbox
|
||||
assert channel.type == "widget"
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_create_channel_with_tags(self, api_client, mailbox):
|
||||
"""Test creating a widget channel with tags."""
|
||||
label = LabelFactory(mailbox=mailbox, name="Widget Inquiries")
|
||||
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {
|
||||
"name": "My Widget with Tags",
|
||||
"type": "widget",
|
||||
"settings": {
|
||||
"subject_template": "Message from {referer_domain}",
|
||||
"tags": [str(label.id)],
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert str(label.id) in response.data["settings"]["tags"]
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_create_channel_with_invalid_tag_uuid(self, api_client, mailbox):
|
||||
"""Test creating a channel with an invalid tag UUID fails."""
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {
|
||||
"name": "Widget with Invalid Tags",
|
||||
"type": "widget",
|
||||
"settings": {
|
||||
"tags": ["not-a-valid-uuid", "also-invalid"],
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "settings" in response.data
|
||||
assert "tags" in response.data["settings"]
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_create_channel_with_nonexistent_tag(self, api_client, mailbox):
|
||||
"""Test creating a channel with a tag that doesn't exist fails."""
|
||||
nonexistent_id = str(uuid.uuid4())
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {
|
||||
"name": "Widget with Missing Tags",
|
||||
"type": "widget",
|
||||
"settings": {
|
||||
"tags": [nonexistent_id],
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "settings" in response.data
|
||||
assert "tags" in response.data["settings"]
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_create_channel_with_tag_from_other_mailbox(self, api_client, mailbox):
|
||||
"""Test creating a channel with a tag from another mailbox fails."""
|
||||
other_mailbox = MailboxFactory()
|
||||
other_label = LabelFactory(mailbox=other_mailbox, name="Other Label")
|
||||
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {
|
||||
"name": "Widget with Wrong Mailbox Tag",
|
||||
"type": "widget",
|
||||
"settings": {
|
||||
"tags": [str(other_label.id)],
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "settings" in response.data
|
||||
assert "tags" in response.data["settings"]
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_create_channel_no_access(self, api_client):
|
||||
"""Test creating a channel for a mailbox the user has no access to."""
|
||||
other_mailbox = MailboxFactory()
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": other_mailbox.id})
|
||||
data = {"name": "Test", "type": "widget", "settings": {}}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_create_channel_viewer_access(self, api_client, user):
|
||||
"""Test creating a channel with viewer role (should fail)."""
|
||||
mailbox = MailboxFactory()
|
||||
mailbox.accesses.create(user=user, role=models.MailboxRoleChoices.VIEWER)
|
||||
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {"name": "Test", "type": "widget", "settings": {}}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_create_channel_unauthorized_type(self, api_client, mailbox):
|
||||
"""Test creating a channel with an unauthorized type."""
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {"name": "Test API Key", "type": "api_key", "settings": {}}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "type" in response.data
|
||||
assert "not authorized" in str(response.data["type"]).lower()
|
||||
assert "api_key" in str(response.data["type"])
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget", "api_key"])
|
||||
def test_create_channel_authorized_type(self, api_client, mailbox):
|
||||
"""Test creating a channel with an authorized type."""
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {"name": "Test Widget", "type": "widget", "settings": {}}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["type"] == "widget"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestChannelRetrieve:
|
||||
"""Test the channel retrieve endpoint."""
|
||||
|
||||
def test_retrieve_channel(self, api_client, mailbox, channel):
|
||||
"""Test retrieving a specific channel."""
|
||||
url = reverse(
|
||||
"mailbox-channels-detail",
|
||||
kwargs={"mailbox_id": mailbox.id, "pk": channel.id},
|
||||
)
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["id"] == str(channel.id)
|
||||
assert response.data["name"] == channel.name
|
||||
|
||||
def test_retrieve_channel_not_found(self, api_client, mailbox):
|
||||
"""Test retrieving a non-existent channel."""
|
||||
url = reverse(
|
||||
"mailbox-channels-detail",
|
||||
kwargs={
|
||||
"mailbox_id": mailbox.id,
|
||||
"pk": "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
)
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestChannelUpdate:
|
||||
"""Test the channel update endpoint."""
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_update_channel(self, api_client, mailbox, channel):
|
||||
"""Test updating a channel."""
|
||||
url = reverse(
|
||||
"mailbox-channels-detail",
|
||||
kwargs={"mailbox_id": mailbox.id, "pk": channel.id},
|
||||
)
|
||||
data = {
|
||||
"name": "Updated Widget Name",
|
||||
"type": "widget",
|
||||
"settings": {
|
||||
"subject_template": "Updated subject from {referer_domain}",
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.put(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["name"] == "Updated Widget Name"
|
||||
assert (
|
||||
response.data["settings"]["subject_template"]
|
||||
== "Updated subject from {referer_domain}"
|
||||
)
|
||||
|
||||
# Verify in database
|
||||
channel.refresh_from_db()
|
||||
assert channel.name == "Updated Widget Name"
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_partial_update_channel(self, api_client, mailbox, channel):
|
||||
"""Test partially updating a channel."""
|
||||
url = reverse(
|
||||
"mailbox-channels-detail",
|
||||
kwargs={"mailbox_id": mailbox.id, "pk": channel.id},
|
||||
)
|
||||
data = {"name": "Partially Updated Name"}
|
||||
|
||||
response = api_client.patch(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["name"] == "Partially Updated Name"
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_update_channel_no_access(self, api_client, mailbox, channel):
|
||||
"""Test updating a channel for a mailbox the user has no admin access to."""
|
||||
# Remove admin access
|
||||
mailbox.accesses.all().delete()
|
||||
|
||||
url = reverse(
|
||||
"mailbox-channels-detail",
|
||||
kwargs={"mailbox_id": mailbox.id, "pk": channel.id},
|
||||
)
|
||||
data = {"name": "Should Not Update"}
|
||||
|
||||
response = api_client.put(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestChannelDelete:
|
||||
"""Test the channel deletion endpoint."""
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_delete_channel(self, api_client, mailbox, channel):
|
||||
"""Test deleting a channel."""
|
||||
channel_id = channel.id
|
||||
url = reverse(
|
||||
"mailbox-channels-detail",
|
||||
kwargs={"mailbox_id": mailbox.id, "pk": channel.id},
|
||||
)
|
||||
|
||||
response = api_client.delete(url)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert not models.Channel.objects.filter(id=channel_id).exists()
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_delete_channel_no_access(self, api_client, mailbox, channel):
|
||||
"""Test deleting a channel without admin access."""
|
||||
# Remove admin access
|
||||
mailbox.accesses.all().delete()
|
||||
|
||||
url = reverse(
|
||||
"mailbox-channels-detail",
|
||||
kwargs={"mailbox_id": mailbox.id, "pk": channel.id},
|
||||
)
|
||||
|
||||
response = api_client.delete(url)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestChannelDomainAdminAccess:
|
||||
"""Test that domain admins can also manage channels."""
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_domain_admin_can_list_channels(self, api_client, user):
|
||||
"""Test that domain admin can list channels."""
|
||||
domain = MailDomainFactory()
|
||||
MailDomainAccessFactory(
|
||||
maildomain=domain,
|
||||
user=user,
|
||||
role=models.MailDomainAccessRoleChoices.ADMIN,
|
||||
)
|
||||
mailbox = MailboxFactory(domain=domain)
|
||||
channel = ChannelFactory(mailbox=mailbox)
|
||||
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data) == 1
|
||||
assert response.data[0]["id"] == str(channel.id)
|
||||
|
||||
@override_settings(FEATURE_MAILBOX_ADMIN_CHANNELS=["widget"])
|
||||
def test_domain_admin_can_create_channel(self, api_client, user):
|
||||
"""Test that domain admin can create a channel."""
|
||||
domain = MailDomainFactory()
|
||||
MailDomainAccessFactory(
|
||||
maildomain=domain,
|
||||
user=user,
|
||||
role=models.MailDomainAccessRoleChoices.ADMIN,
|
||||
)
|
||||
mailbox = MailboxFactory(domain=domain)
|
||||
|
||||
url = reverse("mailbox-channels-list", kwargs={"mailbox_id": mailbox.id})
|
||||
data = {"name": "Domain Admin Widget", "type": "widget", "settings": {}}
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["name"] == "Domain Admin Widget"
|
||||
@@ -23,6 +23,7 @@ pytestmark = pytest.mark.django_db
|
||||
AI_MODEL=None,
|
||||
FEATURE_AI_SUMMARY=False,
|
||||
FEATURE_AI_AUTOLABELS=False,
|
||||
FEATURE_MAILBOX_ADMIN_CHANNELS=[],
|
||||
DRIVE_CONFIG={"base_url": None, "app_name": "Drive"},
|
||||
MAX_OUTGOING_ATTACHMENT_SIZE=20971520, # 20MB
|
||||
MAX_OUTGOING_BODY_SIZE=5242880, # 5MB
|
||||
@@ -48,6 +49,7 @@ def test_api_config(is_authenticated):
|
||||
"AI_ENABLED": False,
|
||||
"FEATURE_AI_SUMMARY": False,
|
||||
"FEATURE_AI_AUTOLABELS": False,
|
||||
"FEATURE_MAILBOX_ADMIN_CHANNELS": [],
|
||||
"SCHEMA_CUSTOM_ATTRIBUTES_USER": {},
|
||||
"SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN": {},
|
||||
"MAX_INCOMING_EMAIL_SIZE": 10485760,
|
||||
|
||||
@@ -302,11 +302,25 @@ class TestInboundWidgetDeliver:
|
||||
in parsed_email["htmlBody"][0]["content"]
|
||||
)
|
||||
|
||||
def test_inbound_widget_deliver_message_e2e(self, api_client, channel):
|
||||
"""Test that message is properly formatted with HTML and metadata."""
|
||||
def test_inbound_widget_deliver_message_e2e(self, api_client):
|
||||
"""Test that message is properly formatted with HTML, metadata, and tags."""
|
||||
|
||||
assert models.Message.objects.count() == 0
|
||||
|
||||
# Create a mailbox with labels and a channel with tags
|
||||
mailbox = factories.MailboxFactory()
|
||||
label1 = factories.LabelFactory(mailbox=mailbox, name="Widget Support")
|
||||
label2 = factories.LabelFactory(mailbox=mailbox, name="Urgent")
|
||||
channel = factories.ChannelFactory(
|
||||
type="widget",
|
||||
mailbox=mailbox,
|
||||
settings={
|
||||
"config": {"enabled": True},
|
||||
"tags": [str(label1.id), str(label2.id)],
|
||||
"subject_template": "Contact from {referer_domain}",
|
||||
},
|
||||
)
|
||||
|
||||
data = {
|
||||
"email": "sender@example.com",
|
||||
"textBody": "Line 1\nLine 2\nLine 3",
|
||||
@@ -322,12 +336,16 @@ class TestInboundWidgetDeliver:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
assert models.Message.objects.count() == 1
|
||||
mailbox = channel.mailbox
|
||||
message = models.Message.objects.first()
|
||||
# Check we have a threadaccess on the right mailbox
|
||||
assert message.thread.accesses.first().mailbox == mailbox
|
||||
assert message.sender.email == "sender@example.com"
|
||||
assert message.subject == "Message from example.com"
|
||||
assert message.subject == "Contact from example.com"
|
||||
|
||||
# Check that channel tags were applied to the thread
|
||||
thread_label_ids = set(message.thread.labels.values_list("id", flat=True))
|
||||
assert label1.id in thread_label_ids
|
||||
assert label2.id in thread_label_ids
|
||||
|
||||
authenticated_user = factories.UserFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from core.api.viewsets.blob import BlobViewSet
|
||||
from core.api.viewsets.channel import ChannelViewSet
|
||||
from core.api.viewsets.config import ConfigView
|
||||
from core.api.viewsets.contacts import ContactViewSet
|
||||
from core.api.viewsets.draft import DraftMessageView
|
||||
@@ -118,6 +119,15 @@ mailbox_message_template_nested_router.register(
|
||||
basename="mailbox-message-templates",
|
||||
)
|
||||
|
||||
# Router for /mailboxes/{mailbox_id}/channels/
|
||||
# allow to manage integration channels for a mailbox
|
||||
mailbox_channel_nested_router = DefaultRouter()
|
||||
mailbox_channel_nested_router.register(
|
||||
r"channels",
|
||||
ChannelViewSet,
|
||||
basename="mailbox-channels",
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
f"api/{settings.API_VERSION}/",
|
||||
@@ -148,6 +158,12 @@ urlpatterns = [
|
||||
mailbox_message_template_nested_router.urls
|
||||
), # Includes /mailboxes/{id}/message-templates/
|
||||
),
|
||||
path(
|
||||
"mailboxes/<uuid:mailbox_id>/",
|
||||
include(
|
||||
mailbox_channel_nested_router.urls
|
||||
), # Includes /mailboxes/{id}/channels/
|
||||
),
|
||||
path(
|
||||
"maildomains/<uuid:maildomain_pk>/",
|
||||
include(maildomain_nested_router.urls),
|
||||
|
||||
@@ -771,6 +771,9 @@ class Base(Configuration):
|
||||
FEATURE_IMPORT_MESSAGES = values.BooleanValue(
|
||||
default=True, environ_name="FEATURE_IMPORT_MESSAGES", environ_prefix=None
|
||||
)
|
||||
FEATURE_MAILBOX_ADMIN_CHANNELS = values.ListValue(
|
||||
default=[], environ_name="FEATURE_MAILBOX_ADMIN_CHANNELS", environ_prefix=None
|
||||
)
|
||||
|
||||
# Logging
|
||||
# We want to make it easy to log to console but by default we log production
|
||||
|
||||
@@ -47,13 +47,17 @@
|
||||
"Accesses": "Accesses",
|
||||
"Actions": "Actions",
|
||||
"Active": "Active",
|
||||
"Add a contact form widget to your website to receive messages directly in your mailbox.": "Add a contact form widget to your website to receive messages directly in your mailbox.",
|
||||
"Add a domain": "Add a domain",
|
||||
"Add a sub-label": "Add a sub-label",
|
||||
"Add attachment from {{driveAppName}}": "Add attachment from {{driveAppName}}",
|
||||
"Add attachments": "Add attachments",
|
||||
"Add label": "Add label",
|
||||
"Add labels": "Add labels",
|
||||
"Add tags": "Add tags",
|
||||
"Add this code snippet to your website to display the feedback widget.": "Add this code snippet to your website to display the feedback widget.",
|
||||
"Address": "Address",
|
||||
"After creating the widget, you will receive the installation code to add to your website.": "After creating the widget, you will receive the installation code to add to your website.",
|
||||
"Addresses": "Addresses",
|
||||
"All messages": "All messages",
|
||||
"An address with this prefix already exists in this domain.": "An address with this prefix already exists in this domain.",
|
||||
@@ -65,7 +69,9 @@
|
||||
"An error occurred while resetting the password.": "An error occurred while resetting the password.",
|
||||
"An error occurred while updating the address.": "An error occurred while updating the address.",
|
||||
"An error occurred while uploading the archive file.": "An error occurred while uploading the archive file.",
|
||||
"An error occurred while saving the integration.": "An error occurred while saving the integration.",
|
||||
"An unexpected error occurred.": "An unexpected error occurred.",
|
||||
"API Key": "API Key",
|
||||
"and {{count}} other users_one": "and 1 other user",
|
||||
"and {{count}} other users_other": "and {{count}} other users",
|
||||
"Archive": "Archive",
|
||||
@@ -77,6 +83,7 @@
|
||||
"Are you sure you want to delete this label? This action is irreversible!": "Are you sure you want to delete this label? This action is irreversible!",
|
||||
"Are you sure you want to delete this mailbox? This action is irreversible!": "Are you sure you want to delete this mailbox? This action is irreversible!",
|
||||
"Are you sure you want to delete this signature? This action is irreversible!": "Are you sure you want to delete this signature? This action is irreversible!",
|
||||
"Are you sure you want to delete this integration? This action is irreversible!": "Are you sure you want to delete this integration? This action is irreversible!",
|
||||
"Are you sure you want to delete this template? This action is irreversible!": "Are you sure you want to delete this template? This action is irreversible!",
|
||||
"Are you sure you want to reset the password?": "Are you sure you want to reset the password?",
|
||||
"At least one recipient is required.": "At least one recipient is required.",
|
||||
@@ -107,8 +114,11 @@
|
||||
"Collapse all": "Collapse all",
|
||||
"Color: ": "Color: ",
|
||||
"Contains the words": "Contains the words",
|
||||
"Choose the type of integration you want to create": "Choose the type of integration you want to create",
|
||||
"Coming soon": "Coming soon",
|
||||
"Content is required": "Content is required",
|
||||
"Copied": "Copied",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copy": "Copy",
|
||||
"Copy all DNS records": "Copy all DNS records",
|
||||
"Copy to clipboard": "Copy to clipboard",
|
||||
@@ -122,8 +132,11 @@
|
||||
"Create a new shared mailbox": "Create a new shared mailbox",
|
||||
"Create a new signature": "Create a new signature",
|
||||
"Create a new signature for {{domain}}": "Create a new signature for {{domain}}",
|
||||
"Create a new integration": "Create a new integration",
|
||||
"Create a new template": "Create a new template",
|
||||
"Create a Widget": "Create a Widget",
|
||||
"Create a simple redirect (Coming soon)": "Create a simple redirect (Coming soon)",
|
||||
"Create integration": "Create integration",
|
||||
"Create the label \"{{label}}\"": "Create the label \"{{label}}\"",
|
||||
"create_mailbox_modal.success.credential_text": "Your Messages credentials are:\n- Email: {{id}}\n- Password: {{password}}\n\nIt will be asked to change your password at your first login.",
|
||||
"Created at": "Created at",
|
||||
@@ -140,6 +153,7 @@
|
||||
"Delete label \"{{label}}\"": "Delete label \"{{label}}\"",
|
||||
"Delete mailbox {{mailbox}}": "Delete mailbox {{mailbox}}",
|
||||
"Delete signature \"{{signature}}\"": "Delete signature \"{{signature}}\"",
|
||||
"Delete integration \"{{name}}\"": "Delete integration \"{{name}}\"",
|
||||
"Delete template \"{{template}}\"": "Delete template \"{{template}}\"",
|
||||
"Description": "Description",
|
||||
"Description must be less than 255 characters.": "Description must be less than 255 characters.",
|
||||
@@ -166,15 +180,18 @@
|
||||
"Edit {{mailbox}} address": "Edit {{mailbox}} address",
|
||||
"Edit signature \"{{signature}}\"": "Edit signature \"{{signature}}\"",
|
||||
"Edit template \"{{template}}\"": "Edit template \"{{template}}\"",
|
||||
"Edit Widget": "Edit Widget",
|
||||
"Email address": "Email address",
|
||||
"EML or MBOX": "EML or MBOX",
|
||||
"Enter the email addresses of the recipients separated by commas": "Enter the email addresses of the recipients separated by commas",
|
||||
"Error while checking DNS records": "Error while checking DNS records",
|
||||
"Error while loading addresses": "Error while loading addresses",
|
||||
"Error while loading signatures": "Error while loading signatures",
|
||||
"Error while loading integrations": "Error while loading integrations",
|
||||
"Error while loading templates": "Error while loading templates",
|
||||
"Expand": "Expand",
|
||||
"Expand all": "Expand all",
|
||||
"Failed to delete integration.": "Failed to delete integration.",
|
||||
"Failed to delete signature.": "Failed to delete signature.",
|
||||
"Failed to delete template.": "Failed to delete template.",
|
||||
"Failed to refresh summary.": "Failed to refresh summary.",
|
||||
@@ -197,6 +214,8 @@
|
||||
"From: ": "From: ",
|
||||
"Full name": "Full name",
|
||||
"Full name is required.": "Full name is required.",
|
||||
"General": "General",
|
||||
"Generate an API key to send messages programmatically from your applications.": "Generate an API key to send messages programmatically from your applications.",
|
||||
"Generating summary...": "Generating summary...",
|
||||
"How to allow IMAP connections from your account {{name}}?": "How to allow IMAP connections from your account {{name}}?",
|
||||
"I confirm that this address corresponds to the real identity of a colleague, and I commit to deactivating it when their position ends.": "I confirm that this address corresponds to the real identity of a colleague, and I commit to deactivating it when their position ends.",
|
||||
@@ -218,6 +237,11 @@
|
||||
"Incorrect": "Incorrect",
|
||||
"Indicate your old email address and your password.": "Indicate your old email address and your password.",
|
||||
"Insert template": "Insert template",
|
||||
"Installation": "Installation",
|
||||
"Integration created!": "Integration created!",
|
||||
"Integration deleted!": "Integration deleted!",
|
||||
"Integration updated!": "Integration updated!",
|
||||
"Integrations": "Integrations",
|
||||
"just now": "just now",
|
||||
"Label \"{{label}}\" assigned to {{count}} threads._one": "Label \"{{label}}\" assigned to this thread.",
|
||||
"Label \"{{label}}\" assigned to {{count}} threads._other": "Label \"{{label}}\" assigned to {{count}} threads.",
|
||||
@@ -230,8 +254,10 @@
|
||||
"Last update: {{timestamp}}": "Last update: {{timestamp}}",
|
||||
"less than a minute ago": "less than a minute ago",
|
||||
"Loading addresses...": "Loading addresses...",
|
||||
"Loading integrations...": "Loading integrations...",
|
||||
"Loading labels...": "Loading labels...",
|
||||
"Loading next threads...": "Loading next threads...",
|
||||
"Loading tags...": "Loading tags...",
|
||||
"Loading signatures...": "Loading signatures...",
|
||||
"Loading templates...": "Loading templates...",
|
||||
"Loading variables...": "Loading variables...",
|
||||
@@ -268,6 +294,7 @@
|
||||
"Name must be a valid domain name.": "Name must be a valid domain name.",
|
||||
"New address": "New address",
|
||||
"New domain": "New domain",
|
||||
"New integration": "New integration",
|
||||
"New message": "New message",
|
||||
"New signature": "New signature",
|
||||
"New template": "New template",
|
||||
@@ -275,12 +302,14 @@
|
||||
"No addresses found": "No addresses found",
|
||||
"No attachments": "No attachments",
|
||||
"No DNS records found": "No DNS records found",
|
||||
"No integration found": "No integration found",
|
||||
"No mailbox.": "No mailbox.",
|
||||
"No results.": "No results.",
|
||||
"No signature": "No signature",
|
||||
"No signatures found": "No signatures found",
|
||||
"No subject": "No subject",
|
||||
"No summary available.": "No summary available.",
|
||||
"No tags selected": "No tags selected",
|
||||
"No template found": "No template found",
|
||||
"No templates available": "No templates available",
|
||||
"No threads.": "No threads.",
|
||||
@@ -303,6 +332,7 @@
|
||||
"Refresh": "Refresh",
|
||||
"Refresh summary": "Refresh summary",
|
||||
"Remove": "Remove",
|
||||
"Remove tag": "Remove tag",
|
||||
"Remove report": "Remove report",
|
||||
"Remove spam report": "Remove spam report",
|
||||
"Remove spam report from {{count}} threads_one": "Remove spam report from {{count}} thread",
|
||||
@@ -317,11 +347,14 @@
|
||||
"Reset password of {{mailbox}}": "Reset password of {{mailbox}}",
|
||||
"Retry": "Retry",
|
||||
"Save": "Save",
|
||||
"Save changes": "Save changes",
|
||||
"Save into your {{driveAppName}}'s workspace": "Save into your {{driveAppName}}'s workspace",
|
||||
"Saving...": "Saving...",
|
||||
"Search": "Search",
|
||||
"Search a label": "Search a label",
|
||||
"Search a tag": "Search a tag",
|
||||
"Search in messages...": "Search in messages...",
|
||||
"Select tags to automatically apply to messages received from this widget.": "Select tags to automatically apply to messages received from this widget.",
|
||||
"See members of this thread ({{count}} members)_one": "See members of this thread ({{count}} members)",
|
||||
"See members of this thread ({{count}} members)_other": "See members of this thread ({{count}} members)",
|
||||
"Select a parent label": "Select a parent label",
|
||||
@@ -335,6 +368,7 @@
|
||||
"Send Feedback": "Send Feedback",
|
||||
"Sending message...": "Sending message...",
|
||||
"Sent": "Sent",
|
||||
"Settings": "Settings",
|
||||
"Share access": "Share access",
|
||||
"Share the credentials of this mailbox with its user. You must transfer them securely, preferably physically.": "Share the credentials of this mailbox with its user. You must transfer them securely, preferably physically.",
|
||||
"Share the new credentials to the user.": "Share the new credentials to the user.",
|
||||
@@ -358,10 +392,14 @@
|
||||
"Subject": "Subject",
|
||||
"Subject:": "Subject:",
|
||||
"Subject: ": "Subject: ",
|
||||
"Subject template": "Subject template",
|
||||
"Subject template is required.": "Subject template is required.",
|
||||
"Message from {referer_domain}": "Message from {referer_domain}",
|
||||
"Summarize": "Summarize",
|
||||
"Summary": "Summary",
|
||||
"Summary refreshed!": "Summary refreshed!",
|
||||
"Synchronize mailboxes with an identity provider": "Synchronize mailboxes with an identity provider",
|
||||
"Tags": "Tags",
|
||||
"Target": "Target",
|
||||
"Target email": "Target email",
|
||||
"Template created!": "Template created!",
|
||||
@@ -382,6 +420,7 @@
|
||||
"The shared mailbox <strong>{{mailboxAddress}}</strong> has been created successfully.": "The shared mailbox <strong>{{mailboxAddress}}</strong> has been created successfully.",
|
||||
"The upload failed. Please try again.": "The upload failed. Please try again.",
|
||||
"These DNS records must be configured on the domain <strong>{{domain}}</strong> for the mail system to work properly. If you don't know how to update them, please contact your technical service provider or system administrator.": "These DNS records must be configured on the domain <strong>{{domain}}</strong> for the mail system to work properly. If you don't know how to update them, please contact your technical service provider or system administrator.",
|
||||
"These tags will be automatically applied to every incoming message from the widget.": "These tags will be automatically applied to every incoming message from the widget.",
|
||||
"This action cannot be undone and the user will need the new password to access its mailbox.": "This action cannot be undone and the user will need the new password to access its mailbox.",
|
||||
"This contact's identity could not be verified. Proceed with caution.": "This contact's identity could not be verified. Proceed with caution.",
|
||||
"This description will be used by the AI to automatically assign this label to your messages.": "This description will be used by the AI to automatically assign this label to your messages.",
|
||||
@@ -392,6 +431,7 @@
|
||||
"This message has been deleted.": "This message has been deleted.",
|
||||
"This message has not been delivered.": "This message has not been delivered.",
|
||||
"This message is being delivered.": "This message is being delivered.",
|
||||
"This name is for internal use only and will not be visible to users.": "This name is for internal use only and will not be visible to users.",
|
||||
"This signature is forced": "This signature is forced",
|
||||
"This thread has been reported as spam.": "This thread has been reported as spam.",
|
||||
"This thread has been reported as spam. For your security, downloading attachments has been disabled.": "This thread has been reported as spam. For your security, downloading attachments has been disabled.",
|
||||
@@ -406,6 +446,7 @@
|
||||
"Trash": "Trash",
|
||||
"Type": "Type",
|
||||
"Unable to copy credentials.": "Unable to copy credentials.",
|
||||
"Unable to copy to clipboard.": "Unable to copy to clipboard.",
|
||||
"Unarchive": "Unarchive",
|
||||
"Unarchive {{count}} threads_one": "Unarchive {{count}} thread",
|
||||
"Unarchive {{count}} threads_other": "Unarchive {{count}} threads",
|
||||
@@ -424,10 +465,14 @@
|
||||
"Uploading your archive": "Uploading your archive",
|
||||
"Uploading... {{progress}}%": "Uploading... {{progress}}%",
|
||||
"Use \"Send and archive\" by default": "Use \"Send and archive\" by default",
|
||||
"Use {referer_domain} to include the website domain in the subject.": "Use {referer_domain} to include the website domain in the subject.",
|
||||
"Use SSL": "Use SSL",
|
||||
"Value": "Value",
|
||||
"Variables": "Variables",
|
||||
"View full documentation": "View full documentation",
|
||||
"Website Widget": "Website Widget",
|
||||
"While the signature is disabled, it will not be available to the users.": "While the signature is disabled, it will not be available to the users.",
|
||||
"Widget": "Widget",
|
||||
"Yesterday": "Yesterday",
|
||||
"You": "You",
|
||||
"You are the last editor of this thread, you cannot therefore modify your access.": "You are the last editor of this thread, you cannot therefore modify your access.",
|
||||
|
||||
@@ -48,11 +48,14 @@
|
||||
"Actions": "Actions",
|
||||
"Active": "Active",
|
||||
"Add a domain": "Ajout d'un domaine",
|
||||
"Add a contact form widget to your website to receive messages directly in your mailbox.": "Ajoutez un widget de formulaire de contact à votre site web pour recevoir des messages directement dans votre boîte aux lettres.",
|
||||
"Add a sub-label": "Ajouter un sous-libellé",
|
||||
"Add attachment from {{driveAppName}}": "Ajouter une pièce jointe depuis {{driveAppName}}",
|
||||
"Add attachments": "Ajoutez des pièces jointes",
|
||||
"Add label": "Ajouter un libellé",
|
||||
"Add labels": "Ajouter des libellés",
|
||||
"Add tags": "Ajouter des tags",
|
||||
"Add this code snippet to your website to display the feedback widget.": "Ajoutez ce code à votre site web pour afficher le widget de contact.",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresses",
|
||||
"All messages": "Tous les messages",
|
||||
@@ -65,7 +68,10 @@
|
||||
"An error occurred while resetting the password.": "Une erreur s'est produite lors de la réinitialisation du mot de passe.",
|
||||
"An error occurred while updating the address.": "Une erreur est survenue lors de la mise à jour de l'adresse.",
|
||||
"An error occurred while uploading the archive file.": "Une erreur est survenue lors du téléversement de l'archive.",
|
||||
"An unexpected error occurred.": "Une erreur inattendue s’est produite.",
|
||||
"An error occurred while saving the integration.": "Une erreur est survenue lors de la sauvegarde de l'intégration.",
|
||||
"An unexpected error occurred.": "Une erreur inattendue s'est produite.",
|
||||
"API Key": "Clé API",
|
||||
"After creating the widget, you will receive the installation code to add to your website.": "Après avoir créé le widget, vous recevrez le code d'installation à ajouter à votre site web.",
|
||||
"and {{count}} other users_one": "et 1 autre utilisateur",
|
||||
"and {{count}} other users_other": "et {{count}} autres utilisateurs",
|
||||
"Archive": "Archiver",
|
||||
@@ -77,6 +83,7 @@
|
||||
"Are you sure you want to delete this label? This action is irreversible!": "Êtes-vous sûr de vouloir supprimer ce libellé ? Cette action est irréversible !",
|
||||
"Are you sure you want to delete this mailbox? This action is irreversible!": "Êtes-vous sûr de vouloir supprimer cette boîte aux lettres ? Cette action est irréversible !",
|
||||
"Are you sure you want to delete this signature? This action is irreversible!": "Êtes-vous sûr de vouloir supprimer cette signature ? Cette action est irréversible !",
|
||||
"Are you sure you want to delete this integration? This action is irreversible!": "Êtes-vous sûr de vouloir supprimer cette intégration ? Cette action est irréversible !",
|
||||
"Are you sure you want to delete this template? This action is irreversible!": "Êtes-vous sûr de vouloir supprimer ce modèle ? Cette action est irréversible !",
|
||||
"Are you sure you want to reset the password?": "Êtes-vous sûr de vouloir réinitialiser le mot de passe ?",
|
||||
"At least one recipient is required.": "Il faut au moins un destinataire.",
|
||||
@@ -108,7 +115,10 @@
|
||||
"Color: ": "Couleur : ",
|
||||
"Contains the words": "Contient les mots",
|
||||
"Content is required": "Un contenu est requis",
|
||||
"Choose the type of integration you want to create": "Choisissez le type d'intégration que vous souhaitez créer",
|
||||
"Coming soon": "Bientôt disponible",
|
||||
"Copied": "Copié",
|
||||
"Copied to clipboard": "Copié dans le presse-papiers",
|
||||
"Copy": "Copier",
|
||||
"Copy all DNS records": "Copier tous les enregistrements DNS",
|
||||
"Copy to clipboard": "Copier dans le presse-papiers",
|
||||
@@ -117,8 +127,11 @@
|
||||
"Create": "Créer",
|
||||
"Create a Label": "Créer un libellé",
|
||||
"Create a new address @{{domain}}": "Création d'une nouvelle adresse @{{domain}}",
|
||||
"Create a new integration": "Créer une nouvelle intégration",
|
||||
"Create a new label": "Créer un nouveau libellé",
|
||||
"Create a new personal mailbox": "Créer une nouvelle boîte personnelle",
|
||||
"Create a Widget": "Créer un Widget",
|
||||
"Create integration": "Créer l'intégration",
|
||||
"Create a new shared mailbox": "Créer une nouvelle boîte partagée",
|
||||
"Create a new signature": "Créer une nouvelle signature",
|
||||
"Create a new signature for {{domain}}": "Création d'une nouvelle signature pour {{domain}}",
|
||||
@@ -140,6 +153,7 @@
|
||||
"Delete label \"{{label}}\"": "Supprimer le libellé \"{{label}}\"",
|
||||
"Delete mailbox {{mailbox}}": "Supprimer la boîte aux lettres {{mailbox}}",
|
||||
"Delete signature \"{{signature}}\"": "Supprimer la signature \"{{signature}}\"",
|
||||
"Delete integration \"{{name}}\"": "Supprimer l'intégration \"{{name}}\"",
|
||||
"Delete template \"{{template}}\"": "Supprimer le modèle \"{{template}}\"",
|
||||
"Description": "Description",
|
||||
"Description must be less than 255 characters.": "La description ne peut pas excéder 255 caractères.",
|
||||
@@ -166,15 +180,18 @@
|
||||
"Edit {{mailbox}} address": "Modifier l'adresse {{mailbox}}",
|
||||
"Edit signature \"{{signature}}\"": "Modifier la signature \"{{signature}}\"",
|
||||
"Edit template \"{{template}}\"": "Modifier le modèle \"{{template}}\"",
|
||||
"Edit Widget": "Modifier le Widget",
|
||||
"Email address": "Adresse mail",
|
||||
"EML or MBOX": "EML ou MBOX",
|
||||
"Enter the email addresses of the recipients separated by commas": "Entrez les adresses e-mail des destinataires séparés par des virgules",
|
||||
"Error while checking DNS records": "Erreur lors de la vérification des enregistrements DNS",
|
||||
"Error while loading addresses": "Erreur lors du chargement des adresses",
|
||||
"Error while loading signatures": "Erreur lors du chargement des signatures",
|
||||
"Error while loading integrations": "Erreur lors du chargement des intégrations",
|
||||
"Error while loading templates": "Erreur lors du chargement des modèles",
|
||||
"Expand": "Développer",
|
||||
"Expand all": "Tout développer",
|
||||
"Failed to delete integration.": "Erreur lors de la suppression de l'intégration.",
|
||||
"Failed to delete signature.": "Erreur lors de la suppression de la signature.",
|
||||
"Failed to delete template.": "Erreur lors de la suppression du modèle.",
|
||||
"Failed to refresh summary.": "Erreur lors de la mise à jour du résumé.",
|
||||
@@ -197,6 +214,8 @@
|
||||
"From: ": "De : ",
|
||||
"Full name": "Nom complet",
|
||||
"Full name is required.": "Le nom complet est requis.",
|
||||
"General": "Général",
|
||||
"Generate an API key to send messages programmatically from your applications.": "Générez une clé API pour envoyer des messages de façon programmatique depuis vos applications.",
|
||||
"Generating summary...": "Génération du résumé en cours...",
|
||||
"How to allow IMAP connections from your account {{name}}?": "Comment autoriser les connexions IMAP depuis votre compte {{name}} ?",
|
||||
"I confirm that this address corresponds to the real identity of a colleague, and I commit to deactivating it when their position ends.": "Je confirme que cette adresse correspond à l'identité d'une personne physique travaillant avec moi, et m'engage à la désactiver quand son poste prendra fin.",
|
||||
@@ -218,6 +237,11 @@
|
||||
"Incorrect": "Incorrect",
|
||||
"Indicate your old email address and your password.": "Indiquez votre ancienne adresse mail et votre mot de passe.",
|
||||
"Insert template": "Insérer un modèle",
|
||||
"Installation": "Installation",
|
||||
"Integration created!": "Intégration créée !",
|
||||
"Integration deleted!": "Intégration supprimée !",
|
||||
"Integration updated!": "Intégration mise à jour !",
|
||||
"Integrations": "Intégrations",
|
||||
"just now": "à l'instant",
|
||||
"Label \"{{label}}\" assigned to {{count}} threads._one": "Libellé \"{{label}}\" assigné à la conversation.",
|
||||
"Label \"{{label}}\" assigned to {{count}} threads._other": "Libellé \"{{label}}\" assigné à {{count}} conversations.",
|
||||
@@ -230,8 +254,10 @@
|
||||
"Last update: {{timestamp}}": "Dernière mise à jour : {{timestamp}}",
|
||||
"less than a minute ago": "il y a moins d'une minute",
|
||||
"Loading addresses...": "Chargement des adresses...",
|
||||
"Loading integrations...": "Chargement des intégrations...",
|
||||
"Loading labels...": "Chargement des libellés...",
|
||||
"Loading next threads...": "Chargement des conversations suivantes...",
|
||||
"Loading tags...": "Chargement des tags...",
|
||||
"Loading signatures...": "Chargement des signatures...",
|
||||
"Loading templates...": "Chargement des modèles...",
|
||||
"Loading variables...": "Chargement des variables...",
|
||||
@@ -268,6 +294,7 @@
|
||||
"Name must be a valid domain name.": "Le nom doit être un nom de domaine valide.",
|
||||
"New address": "Nouvelle adresse",
|
||||
"New domain": "Nouveau domaine",
|
||||
"New integration": "Nouvelle intégration",
|
||||
"New message": "Nouveau message",
|
||||
"New signature": "Nouvelle signature",
|
||||
"New template": "Nouveau modèle",
|
||||
@@ -275,8 +302,10 @@
|
||||
"No addresses found": "Aucune adresse trouvée",
|
||||
"No attachments": "Aucune pièce jointe",
|
||||
"No DNS records found": "Aucun enregistrement DNS trouvé",
|
||||
"No integration found": "Aucune intégration trouvée",
|
||||
"No mailbox.": "Aucune boîte aux lettres.",
|
||||
"No results.": "Aucun résultat.",
|
||||
"No tags selected": "Aucun tag sélectionné",
|
||||
"No signature": "Aucune signature",
|
||||
"No signatures found": "Aucune signature trouvée",
|
||||
"No subject": "Aucun objet",
|
||||
@@ -303,6 +332,7 @@
|
||||
"Refresh": "Actualiser",
|
||||
"Refresh summary": "Actualiser le résumé",
|
||||
"Remove": "Supprimer",
|
||||
"Remove tag": "Supprimer le tag",
|
||||
"Remove report": "Annuler le signalement",
|
||||
"Remove spam report": "Annuler le signalement spam",
|
||||
"Remove spam report from {{count}} threads_one": "Annuler le signalement spam de la conversation",
|
||||
@@ -317,11 +347,14 @@
|
||||
"Reset password of {{mailbox}}": "Réinitialiser le mot de passe de {{mailbox}}",
|
||||
"Retry": "Réessayer",
|
||||
"Save": "Enregistrer",
|
||||
"Save changes": "Enregistrer les modifications",
|
||||
"Save into your {{driveAppName}}'s workspace": "Sauvegarder dans votre espace de travail {{driveAppName}}",
|
||||
"Saving...": "Enregistrement en cours...",
|
||||
"Search": "Rechercher",
|
||||
"Search a label": "Rechercher un libellé",
|
||||
"Search a tag": "Rechercher un tag",
|
||||
"Search in messages...": "Rechercher dans vos messages...",
|
||||
"Select tags to automatically apply to messages received from this widget.": "Sélectionnez les tags à appliquer automatiquement aux messages reçus depuis ce widget.",
|
||||
"See members of this thread ({{count}} members)_one": "Voir les membres de cette conversation ({{count}} membre)",
|
||||
"See members of this thread ({{count}} members)_other": "Voir les membres de cette conversation ({{count}} membres)",
|
||||
"Select a parent label": "Sélectionnez un libellé parent",
|
||||
@@ -335,6 +368,7 @@
|
||||
"Send Feedback": "Envoyer le message",
|
||||
"Sending message...": "Envoi du message en cours...",
|
||||
"Sent": "Envoyés",
|
||||
"Settings": "Paramètres",
|
||||
"Share access": "Partager l'accès",
|
||||
"Share the credentials of this mailbox with its user. You must transfer them securely, preferably physically.": "Transmettez les identifiants de cette boîte mail à son utilisateur de façon sécurisée, de préférence physiquement.",
|
||||
"Share the new credentials to the user.": "Transmettez les nouveaux identifiants à l'utilisateur.",
|
||||
@@ -358,10 +392,14 @@
|
||||
"Subject": "Objet",
|
||||
"Subject:": "Objet :",
|
||||
"Subject: ": "Objet : ",
|
||||
"Subject template": "Modèle d'objet",
|
||||
"Subject template is required.": "Le modèle d'objet est requis.",
|
||||
"Message from {referer_domain}": "Message de {referer_domain}",
|
||||
"Summarize": "Résumer",
|
||||
"Summary": "Résumé",
|
||||
"Summary refreshed!": "Résumé mis à jour !",
|
||||
"Synchronize mailboxes with an identity provider": "Synchroniser les boîtes aux lettres avec un fournisseur d'identité",
|
||||
"Tags": "Tags",
|
||||
"Target": "Cible",
|
||||
"Target email": "Adresse de destination",
|
||||
"Template created!": "Modèle créé !",
|
||||
@@ -382,6 +420,7 @@
|
||||
"The shared mailbox <strong>{{mailboxAddress}}</strong> has been created successfully.": "L'adresse partagée <strong>{{mailboxAddress}}</strong> a été créée avec succès.",
|
||||
"The upload failed. Please try again.": "Le téléversement a échoué. Veuillez réessayer.",
|
||||
"These DNS records must be configured on the domain <strong>{{domain}}</strong> for the mail system to work properly. If you don't know how to update them, please contact your technical service provider or system administrator.": "Ces enregistrements DNS doivent être configurés sur le domaine <strong>{{domain}}</strong> pour que le système de messagerie fonctionne correctement. Si vous ne savez pas comment les mettre à jour, contactez votre prestataire technique ou votre administrateur système.",
|
||||
"These tags will be automatically applied to every incoming message from the widget.": "Ces tags seront automatiquement appliqués à chaque message entrant provenant du widget.",
|
||||
"This action cannot be undone and the user will need the new password to access its mailbox.": "Cette action est irréversible et l'utilisateur aura besoin du nouveau mot de passe pour accéder à sa boîte aux lettres.",
|
||||
"This contact's identity could not be verified. Proceed with caution.": "L'identité de ce contact n'a pas pu être vérifiée. Faites attention.",
|
||||
"This description will be used by the AI to automatically assign this label to your messages.": "Cette description sera utilisée par l'IA pour assigner automatiquement cette étiquette à vos messages.",
|
||||
@@ -392,6 +431,7 @@
|
||||
"This message has been deleted.": "Ce message a été supprimé.",
|
||||
"This message has not been delivered.": "Ce message n'a pas pu être délivré.",
|
||||
"This message is being delivered.": "Ce message est en cours d'envoi.",
|
||||
"This name is for internal use only and will not be visible to users.": "Ce nom est réservé à un usage interne et ne sera pas visible par les utilisateurs.",
|
||||
"This signature is forced": "Cette signature est forcée",
|
||||
"This thread has been reported as spam.": "Cette conversation a été signalée comme spam.",
|
||||
"This thread has been reported as spam. For your security, downloading attachments has been disabled.": "Cette conversation a été signalée comme spam. Pour votre sécurité, le téléchargement des pièces jointes a été désactivé.",
|
||||
@@ -406,6 +446,7 @@
|
||||
"Trash": "Corbeille",
|
||||
"Type": "Type",
|
||||
"Unable to copy credentials.": "Impossible de copier les identifiants.",
|
||||
"Unable to copy to clipboard.": "Impossible de copier dans le presse-papiers.",
|
||||
"Unarchive": "Désarchiver",
|
||||
"Unarchive {{count}} threads_one": "Désarchiver la conversation",
|
||||
"Unarchive {{count}} threads_other": "Désarchiver {{count}} conversations",
|
||||
@@ -424,10 +465,14 @@
|
||||
"Uploading your archive": "Téléversement de votre archive",
|
||||
"Uploading... {{progress}}%": "Téléversement en cours... {{progress}}%",
|
||||
"Use \"Send and archive\" by default": "Utiliser \"Envoyer et archiver\" par défaut",
|
||||
"Use {referer_domain} to include the website domain in the subject.": "Utilisez {referer_domain} pour inclure le domaine du site web dans l'objet.",
|
||||
"Use SSL": "Utiliser SSL",
|
||||
"Value": "Valeur",
|
||||
"Variables": "Variables",
|
||||
"View full documentation": "Voir la documentation complète",
|
||||
"Website Widget": "Widget de site web",
|
||||
"While the signature is disabled, it will not be available to the users.": "Tant que la signature est désactivée, elle ne sera pas disponible pour les utilisateurs.",
|
||||
"Widget": "Widget",
|
||||
"Yesterday": "Hier",
|
||||
"You": "Vous",
|
||||
"You are the last editor of this thread, you cannot therefore modify your access.": "Vous êtes le dernier éditeur de cette conversation, vous ne pouvez donc pas modifier votre accès.",
|
||||
|
||||
@@ -0,0 +1,891 @@
|
||||
/**
|
||||
* Generated by orval v7.17.2 🍺
|
||||
* Do not edit manually.
|
||||
* messages API
|
||||
* This is the messages API schema.
|
||||
* OpenAPI spec version: 1.0.0 (v1.0)
|
||||
*/
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type {
|
||||
DataTag,
|
||||
DefinedInitialDataOptions,
|
||||
DefinedUseQueryResult,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UndefinedInitialDataOptions,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type {
|
||||
Channel,
|
||||
ChannelRequest,
|
||||
PatchedChannelRequest,
|
||||
} from ".././models";
|
||||
|
||||
import { fetchAPI } from "../../fetch-api";
|
||||
|
||||
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
||||
|
||||
/**
|
||||
* Manage integration channels for a mailbox
|
||||
*/
|
||||
export type mailboxesChannelsListResponse200 = {
|
||||
data: Channel[];
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsListResponseSuccess =
|
||||
mailboxesChannelsListResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type mailboxesChannelsListResponse =
|
||||
mailboxesChannelsListResponseSuccess;
|
||||
|
||||
export const getMailboxesChannelsListUrl = (mailboxId: string) => {
|
||||
return `/api/v1.0/mailboxes/${mailboxId}/channels/`;
|
||||
};
|
||||
|
||||
export const mailboxesChannelsList = async (
|
||||
mailboxId: string,
|
||||
options?: RequestInit,
|
||||
): Promise<mailboxesChannelsListResponse> => {
|
||||
return fetchAPI<mailboxesChannelsListResponse>(
|
||||
getMailboxesChannelsListUrl(mailboxId),
|
||||
{
|
||||
...options,
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getMailboxesChannelsListQueryKey = (mailboxId?: string) => {
|
||||
return [`/api/v1.0/mailboxes/${mailboxId}/channels/`] as const;
|
||||
};
|
||||
|
||||
export const getMailboxesChannelsListQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getMailboxesChannelsListQueryKey(mailboxId);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>
|
||||
> = ({ signal }) =>
|
||||
mailboxesChannelsList(mailboxId, { signal, ...requestOptions });
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!mailboxId,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
};
|
||||
|
||||
export type MailboxesChannelsListQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>
|
||||
>;
|
||||
export type MailboxesChannelsListQueryError = unknown;
|
||||
|
||||
export function useMailboxesChannelsList<
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
options: {
|
||||
query: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): DefinedUseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
export function useMailboxesChannelsList<
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
export function useMailboxesChannelsList<
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
|
||||
export function useMailboxesChannelsList<
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsList>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
} {
|
||||
const queryOptions = getMailboxesChannelsListQueryOptions(mailboxId, options);
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
|
||||
TData,
|
||||
TError
|
||||
> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage integration channels for a mailbox
|
||||
*/
|
||||
export type mailboxesChannelsCreateResponse201 = {
|
||||
data: Channel;
|
||||
status: 201;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsCreateResponse400 = {
|
||||
data: void;
|
||||
status: 400;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsCreateResponse403 = {
|
||||
data: void;
|
||||
status: 403;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsCreateResponseSuccess =
|
||||
mailboxesChannelsCreateResponse201 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type mailboxesChannelsCreateResponseError = (
|
||||
| mailboxesChannelsCreateResponse400
|
||||
| mailboxesChannelsCreateResponse403
|
||||
) & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsCreateResponse =
|
||||
| mailboxesChannelsCreateResponseSuccess
|
||||
| mailboxesChannelsCreateResponseError;
|
||||
|
||||
export const getMailboxesChannelsCreateUrl = (mailboxId: string) => {
|
||||
return `/api/v1.0/mailboxes/${mailboxId}/channels/`;
|
||||
};
|
||||
|
||||
export const mailboxesChannelsCreate = async (
|
||||
mailboxId: string,
|
||||
channelRequest: ChannelRequest,
|
||||
options?: RequestInit,
|
||||
): Promise<mailboxesChannelsCreateResponse> => {
|
||||
return fetchAPI<mailboxesChannelsCreateResponse>(
|
||||
getMailboxesChannelsCreateUrl(mailboxId),
|
||||
{
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(channelRequest),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getMailboxesChannelsCreateMutationOptions = <
|
||||
TError = void,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsCreate>>,
|
||||
TError,
|
||||
{ mailboxId: string; data: ChannelRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsCreate>>,
|
||||
TError,
|
||||
{ mailboxId: string; data: ChannelRequest },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["mailboxesChannelsCreate"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsCreate>>,
|
||||
{ mailboxId: string; data: ChannelRequest }
|
||||
> = (props) => {
|
||||
const { mailboxId, data } = props ?? {};
|
||||
|
||||
return mailboxesChannelsCreate(mailboxId, data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type MailboxesChannelsCreateMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsCreate>>
|
||||
>;
|
||||
export type MailboxesChannelsCreateMutationBody = ChannelRequest;
|
||||
export type MailboxesChannelsCreateMutationError = void;
|
||||
|
||||
export const useMailboxesChannelsCreate = <TError = void, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsCreate>>,
|
||||
TError,
|
||||
{ mailboxId: string; data: ChannelRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsCreate>>,
|
||||
TError,
|
||||
{ mailboxId: string; data: ChannelRequest },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getMailboxesChannelsCreateMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
/**
|
||||
* Manage integration channels for a mailbox
|
||||
*/
|
||||
export type mailboxesChannelsRetrieveResponse200 = {
|
||||
data: Channel;
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsRetrieveResponseSuccess =
|
||||
mailboxesChannelsRetrieveResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type mailboxesChannelsRetrieveResponse =
|
||||
mailboxesChannelsRetrieveResponseSuccess;
|
||||
|
||||
export const getMailboxesChannelsRetrieveUrl = (
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
) => {
|
||||
return `/api/v1.0/mailboxes/${mailboxId}/channels/${id}/`;
|
||||
};
|
||||
|
||||
export const mailboxesChannelsRetrieve = async (
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
options?: RequestInit,
|
||||
): Promise<mailboxesChannelsRetrieveResponse> => {
|
||||
return fetchAPI<mailboxesChannelsRetrieveResponse>(
|
||||
getMailboxesChannelsRetrieveUrl(mailboxId, id),
|
||||
{
|
||||
...options,
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getMailboxesChannelsRetrieveQueryKey = (
|
||||
mailboxId?: string,
|
||||
id?: string,
|
||||
) => {
|
||||
return [`/api/v1.0/mailboxes/${mailboxId}/channels/${id}/`] as const;
|
||||
};
|
||||
|
||||
export const getMailboxesChannelsRetrieveQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getMailboxesChannelsRetrieveQueryKey(mailboxId, id);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>
|
||||
> = ({ signal }) =>
|
||||
mailboxesChannelsRetrieve(mailboxId, id, { signal, ...requestOptions });
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!(mailboxId && id),
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
};
|
||||
|
||||
export type MailboxesChannelsRetrieveQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>
|
||||
>;
|
||||
export type MailboxesChannelsRetrieveQueryError = unknown;
|
||||
|
||||
export function useMailboxesChannelsRetrieve<
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
options: {
|
||||
query: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): DefinedUseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
export function useMailboxesChannelsRetrieve<
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
export function useMailboxesChannelsRetrieve<
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
|
||||
export function useMailboxesChannelsRetrieve<
|
||||
TData = Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsRetrieve>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
} {
|
||||
const queryOptions = getMailboxesChannelsRetrieveQueryOptions(
|
||||
mailboxId,
|
||||
id,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
|
||||
TData,
|
||||
TError
|
||||
> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage integration channels for a mailbox
|
||||
*/
|
||||
export type mailboxesChannelsUpdateResponse200 = {
|
||||
data: Channel;
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsUpdateResponse400 = {
|
||||
data: void;
|
||||
status: 400;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsUpdateResponse403 = {
|
||||
data: void;
|
||||
status: 403;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsUpdateResponse404 = {
|
||||
data: void;
|
||||
status: 404;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsUpdateResponseSuccess =
|
||||
mailboxesChannelsUpdateResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type mailboxesChannelsUpdateResponseError = (
|
||||
| mailboxesChannelsUpdateResponse400
|
||||
| mailboxesChannelsUpdateResponse403
|
||||
| mailboxesChannelsUpdateResponse404
|
||||
) & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsUpdateResponse =
|
||||
| mailboxesChannelsUpdateResponseSuccess
|
||||
| mailboxesChannelsUpdateResponseError;
|
||||
|
||||
export const getMailboxesChannelsUpdateUrl = (
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
) => {
|
||||
return `/api/v1.0/mailboxes/${mailboxId}/channels/${id}/`;
|
||||
};
|
||||
|
||||
export const mailboxesChannelsUpdate = async (
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
channelRequest: ChannelRequest,
|
||||
options?: RequestInit,
|
||||
): Promise<mailboxesChannelsUpdateResponse> => {
|
||||
return fetchAPI<mailboxesChannelsUpdateResponse>(
|
||||
getMailboxesChannelsUpdateUrl(mailboxId, id),
|
||||
{
|
||||
...options,
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(channelRequest),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getMailboxesChannelsUpdateMutationOptions = <
|
||||
TError = void,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsUpdate>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string; data: ChannelRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsUpdate>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string; data: ChannelRequest },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["mailboxesChannelsUpdate"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsUpdate>>,
|
||||
{ mailboxId: string; id: string; data: ChannelRequest }
|
||||
> = (props) => {
|
||||
const { mailboxId, id, data } = props ?? {};
|
||||
|
||||
return mailboxesChannelsUpdate(mailboxId, id, data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type MailboxesChannelsUpdateMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsUpdate>>
|
||||
>;
|
||||
export type MailboxesChannelsUpdateMutationBody = ChannelRequest;
|
||||
export type MailboxesChannelsUpdateMutationError = void;
|
||||
|
||||
export const useMailboxesChannelsUpdate = <TError = void, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsUpdate>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string; data: ChannelRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsUpdate>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string; data: ChannelRequest },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getMailboxesChannelsUpdateMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
/**
|
||||
* Manage integration channels for a mailbox
|
||||
*/
|
||||
export type mailboxesChannelsPartialUpdateResponse200 = {
|
||||
data: Channel;
|
||||
status: 200;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsPartialUpdateResponseSuccess =
|
||||
mailboxesChannelsPartialUpdateResponse200 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type mailboxesChannelsPartialUpdateResponse =
|
||||
mailboxesChannelsPartialUpdateResponseSuccess;
|
||||
|
||||
export const getMailboxesChannelsPartialUpdateUrl = (
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
) => {
|
||||
return `/api/v1.0/mailboxes/${mailboxId}/channels/${id}/`;
|
||||
};
|
||||
|
||||
export const mailboxesChannelsPartialUpdate = async (
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
patchedChannelRequest: PatchedChannelRequest,
|
||||
options?: RequestInit,
|
||||
): Promise<mailboxesChannelsPartialUpdateResponse> => {
|
||||
return fetchAPI<mailboxesChannelsPartialUpdateResponse>(
|
||||
getMailboxesChannelsPartialUpdateUrl(mailboxId, id),
|
||||
{
|
||||
...options,
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(patchedChannelRequest),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getMailboxesChannelsPartialUpdateMutationOptions = <
|
||||
TError = unknown,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsPartialUpdate>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string; data: PatchedChannelRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsPartialUpdate>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string; data: PatchedChannelRequest },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["mailboxesChannelsPartialUpdate"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsPartialUpdate>>,
|
||||
{ mailboxId: string; id: string; data: PatchedChannelRequest }
|
||||
> = (props) => {
|
||||
const { mailboxId, id, data } = props ?? {};
|
||||
|
||||
return mailboxesChannelsPartialUpdate(mailboxId, id, data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type MailboxesChannelsPartialUpdateMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsPartialUpdate>>
|
||||
>;
|
||||
export type MailboxesChannelsPartialUpdateMutationBody = PatchedChannelRequest;
|
||||
export type MailboxesChannelsPartialUpdateMutationError = unknown;
|
||||
|
||||
export const useMailboxesChannelsPartialUpdate = <
|
||||
TError = unknown,
|
||||
TContext = unknown,
|
||||
>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsPartialUpdate>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string; data: PatchedChannelRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsPartialUpdate>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string; data: PatchedChannelRequest },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions =
|
||||
getMailboxesChannelsPartialUpdateMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
/**
|
||||
* Manage integration channels for a mailbox
|
||||
*/
|
||||
export type mailboxesChannelsDestroyResponse204 = {
|
||||
data: void;
|
||||
status: 204;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsDestroyResponse403 = {
|
||||
data: void;
|
||||
status: 403;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsDestroyResponse404 = {
|
||||
data: void;
|
||||
status: 404;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsDestroyResponseSuccess =
|
||||
mailboxesChannelsDestroyResponse204 & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type mailboxesChannelsDestroyResponseError = (
|
||||
| mailboxesChannelsDestroyResponse403
|
||||
| mailboxesChannelsDestroyResponse404
|
||||
) & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type mailboxesChannelsDestroyResponse =
|
||||
| mailboxesChannelsDestroyResponseSuccess
|
||||
| mailboxesChannelsDestroyResponseError;
|
||||
|
||||
export const getMailboxesChannelsDestroyUrl = (
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
) => {
|
||||
return `/api/v1.0/mailboxes/${mailboxId}/channels/${id}/`;
|
||||
};
|
||||
|
||||
export const mailboxesChannelsDestroy = async (
|
||||
mailboxId: string,
|
||||
id: string,
|
||||
options?: RequestInit,
|
||||
): Promise<mailboxesChannelsDestroyResponse> => {
|
||||
return fetchAPI<mailboxesChannelsDestroyResponse>(
|
||||
getMailboxesChannelsDestroyUrl(mailboxId, id),
|
||||
{
|
||||
...options,
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getMailboxesChannelsDestroyMutationOptions = <
|
||||
TError = void,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsDestroy>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsDestroy>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["mailboxesChannelsDestroy"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsDestroy>>,
|
||||
{ mailboxId: string; id: string }
|
||||
> = (props) => {
|
||||
const { mailboxId, id } = props ?? {};
|
||||
|
||||
return mailboxesChannelsDestroy(mailboxId, id, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type MailboxesChannelsDestroyMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsDestroy>>
|
||||
>;
|
||||
|
||||
export type MailboxesChannelsDestroyMutationError = void;
|
||||
|
||||
export const useMailboxesChannelsDestroy = <TError = void, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsDestroy>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof mailboxesChannelsDestroy>>,
|
||||
TError,
|
||||
{ mailboxId: string; id: string },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getMailboxesChannelsDestroyMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ export * from "./import/import";
|
||||
export * from "./labels/labels";
|
||||
export * from "./mailboxes/mailboxes";
|
||||
export * from "./mailbox-accesses/mailbox-accesses";
|
||||
export * from "./channels/channels";
|
||||
export * from "./maildomains/maildomains";
|
||||
export * from "./maildomain-accesses/maildomain-accesses";
|
||||
export * from "./placeholders/placeholders";
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Generated by orval v7.17.2 🍺
|
||||
* Do not edit manually.
|
||||
* messages API
|
||||
* This is the messages API schema.
|
||||
* OpenAPI spec version: 1.0.0 (v1.0)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serialize Channel model.
|
||||
*/
|
||||
export interface Channel {
|
||||
/** primary key for the record as UUID */
|
||||
readonly id: string;
|
||||
/**
|
||||
* Human-readable name for this channel
|
||||
* @maxLength 255
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Type of channel
|
||||
* @maxLength 255
|
||||
*/
|
||||
type?: string;
|
||||
/** Channel-specific configuration settings */
|
||||
settings?: unknown;
|
||||
/**
|
||||
* primary key for the record as UUID
|
||||
* @nullable
|
||||
*/
|
||||
readonly mailbox: string | null;
|
||||
/**
|
||||
* primary key for the record as UUID
|
||||
* @nullable
|
||||
*/
|
||||
readonly maildomain: string | null;
|
||||
/** date and time at which a record was created */
|
||||
readonly created_at: string;
|
||||
/** date and time at which a record was last updated */
|
||||
readonly updated_at: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Generated by orval v7.17.2 🍺
|
||||
* Do not edit manually.
|
||||
* messages API
|
||||
* This is the messages API schema.
|
||||
* OpenAPI spec version: 1.0.0 (v1.0)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serialize Channel model.
|
||||
*/
|
||||
export interface ChannelRequest {
|
||||
/**
|
||||
* Human-readable name for this channel
|
||||
* @minLength 1
|
||||
* @maxLength 255
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Type of channel
|
||||
* @minLength 1
|
||||
* @maxLength 255
|
||||
*/
|
||||
type?: string;
|
||||
/** Channel-specific configuration settings */
|
||||
settings?: unknown;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export type ConfigRetrieve200 = {
|
||||
readonly AI_ENABLED: boolean;
|
||||
readonly FEATURE_AI_SUMMARY: boolean;
|
||||
readonly FEATURE_AI_AUTOLABELS: boolean;
|
||||
readonly FEATURE_MAILBOX_ADMIN_CHANNELS: readonly string[];
|
||||
/** The URLs of the Drive external service. */
|
||||
readonly DRIVE?: ConfigRetrieve200DRIVE;
|
||||
readonly SCHEMA_CUSTOM_ATTRIBUTES_USER: ConfigRetrieve200SCHEMACUSTOMATTRIBUTESUSER;
|
||||
|
||||
@@ -10,6 +10,8 @@ export * from "./attachment";
|
||||
export * from "./blob_upload_create201";
|
||||
export * from "./blob_upload_create_body";
|
||||
export * from "./change_flag_request_request";
|
||||
export * from "./channel";
|
||||
export * from "./channel_request";
|
||||
export * from "./config_retrieve200";
|
||||
export * from "./config_retrieve200_driv_e";
|
||||
export * from "./config_retrieve200_schemacustomattributesmaildomai_n";
|
||||
@@ -98,6 +100,7 @@ export * from "./paginated_mailbox_admin_list";
|
||||
export * from "./paginated_thread_access_list";
|
||||
export * from "./paginated_thread_list";
|
||||
export * from "./partial_drive_item";
|
||||
export * from "./patched_channel_request";
|
||||
export * from "./patched_label_request";
|
||||
export * from "./patched_mailbox_access_write_request";
|
||||
export * from "./patched_mailbox_admin_partial_update_payload_request";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Generated by orval v7.17.2 🍺
|
||||
* Do not edit manually.
|
||||
* messages API
|
||||
* This is the messages API schema.
|
||||
* OpenAPI spec version: 1.0.0 (v1.0)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serialize Channel model.
|
||||
*/
|
||||
export interface PatchedChannelRequest {
|
||||
/**
|
||||
* Human-readable name for this channel
|
||||
* @minLength 1
|
||||
* @maxLength 255
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Type of channel
|
||||
* @minLength 1
|
||||
* @maxLength 255
|
||||
*/
|
||||
type?: string;
|
||||
/** Channel-specific configuration settings */
|
||||
settings?: unknown;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { Icon } from "@gouvfr-lasuite/ui-kit";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useModal } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { ModalComposeIntegration } from "../modal-compose-integration";
|
||||
|
||||
export const CreateIntegrationAction = () => {
|
||||
const { t } = useTranslation();
|
||||
const modal = useModal();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => modal.open()}
|
||||
icon={<Icon name="add" />}
|
||||
>
|
||||
{t("New integration")}
|
||||
</Button>
|
||||
<ModalComposeIntegration
|
||||
isOpen={modal.isOpen}
|
||||
onClose={() => modal.close()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import { Icon, IconSize, IconType, Spinner } from "@gouvfr-lasuite/ui-kit";
|
||||
import { Button, Column, DataGrid, useModal, useModals } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Mailbox,
|
||||
Channel,
|
||||
useMailboxesChannelsList,
|
||||
useMailboxesChannelsDestroy,
|
||||
getMailboxesChannelsListUrl
|
||||
} from "@/features/api/gen";
|
||||
import { Banner } from "@/features/ui/components/banner";
|
||||
import { addToast, ToasterItem } from "@/features/ui/components/toaster";
|
||||
import { ModalComposeIntegration } from "../modal-compose-integration";
|
||||
import { handle } from "@/features/utils/errors";
|
||||
|
||||
type IntegrationsDataGridProps = {
|
||||
mailbox: Mailbox;
|
||||
}
|
||||
|
||||
const getChannelTypeLabel = (type: string | undefined, t: (key: string) => string) => {
|
||||
switch (type) {
|
||||
case "widget":
|
||||
return t("Widget");
|
||||
case "api_key":
|
||||
return t("API Key");
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
const getChannelTypeIcon = (type: string | undefined) => {
|
||||
switch (type) {
|
||||
case "widget":
|
||||
return "widgets";
|
||||
case "api_key":
|
||||
return "key";
|
||||
default:
|
||||
return "integration_instructions";
|
||||
}
|
||||
};
|
||||
|
||||
export const IntegrationsDataGrid = ({ mailbox }: IntegrationsDataGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
const modals = useModals();
|
||||
const modal = useModal();
|
||||
const { data: channels, isLoading, error } = useMailboxesChannelsList(
|
||||
mailbox.id,
|
||||
{
|
||||
query: {
|
||||
enabled: !!mailbox.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
const { mutateAsync: deleteChannel, isPending: isDeleting } = useMailboxesChannelsDestroy();
|
||||
const [selectedChannel, setSelectedChannel] = useState<Channel | undefined>();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateChannels = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [getMailboxesChannelsListUrl(mailbox.id)], exact: false });
|
||||
}
|
||||
|
||||
const handleModifyRow = (channel: Channel) => {
|
||||
setSelectedChannel(channel);
|
||||
modal.open();
|
||||
}
|
||||
|
||||
const handleDeleteRow = async (channel: Channel) => {
|
||||
const decision = await modals.deleteConfirmationModal({
|
||||
title: <span className="c__modal__text--centered">{t('Delete integration "{{name}}"', { name: channel.name })}</span>,
|
||||
children: t('Are you sure you want to delete this integration? This action is irreversible!'),
|
||||
});
|
||||
if (decision === 'delete') {
|
||||
try {
|
||||
await deleteChannel({ mailboxId: mailbox.id, id: channel.id });
|
||||
await invalidateChannels();
|
||||
addToast(
|
||||
<ToasterItem type="info">
|
||||
<span>{t("Integration deleted!")}</span>
|
||||
</ToasterItem>,
|
||||
);
|
||||
} catch (error) {
|
||||
handle(error);
|
||||
addToast(
|
||||
<ToasterItem type="error">
|
||||
<span>{t("Failed to delete integration.")}</span>
|
||||
</ToasterItem>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columns: Column<Channel>[] = [
|
||||
{
|
||||
id: "name",
|
||||
headerName: t("Name"),
|
||||
renderCell: ({ row }) => (
|
||||
<div className="flex-row flex-align-center" style={{ gap: "var(--c--globals--spacings--xs)" }}>
|
||||
<Icon name={getChannelTypeIcon(row.type)} type={IconType.OUTLINED} size={IconSize.SMALL} />
|
||||
<span>{row.name}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
headerName: t("Type"),
|
||||
size: 150,
|
||||
renderCell: ({ row }) => getChannelTypeLabel(row.type, t),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
size: 154,
|
||||
headerName: t("Actions"),
|
||||
renderCell: ({ row }) => (
|
||||
<div className="flex-row flex-justify-start" style={{ width: "100%", gap: "var(--c--globals--spacings--2xs)" }}>
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="small"
|
||||
onClick={() => handleModifyRow(row)}
|
||||
>
|
||||
{t("Modify")}
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => handleDeleteRow(row)}
|
||||
disabled={isDeleting}
|
||||
icon={isDeleting ? <Spinner size="sm" /> : <Icon name="delete" size={IconSize.SMALL} />}
|
||||
aria-label={t("Delete")}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="admin-data-grid">
|
||||
<Banner type="info" icon={<Spinner />}>
|
||||
{t("Loading integrations...")}
|
||||
</Banner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="admin-data-grid">
|
||||
<Banner type="error">
|
||||
{t("Error while loading integrations")}
|
||||
</Banner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-page__body">
|
||||
<div className="admin-data-grid">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
rows={channels?.data ?? []}
|
||||
onSortModelChange={() => undefined}
|
||||
enableSorting={false}
|
||||
emptyPlaceholderLabel={t("No integration found")}
|
||||
/>
|
||||
<ModalComposeIntegration
|
||||
isOpen={modal.isOpen}
|
||||
onClose={() => {
|
||||
modal.close();
|
||||
setSelectedChannel(undefined);
|
||||
}}
|
||||
channel={selectedChannel}
|
||||
onSuccess={invalidateChannels}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { useMailboxContext } from "@/features/providers/mailbox";
|
||||
import { IntegrationsDataGrid } from "./integrations-data-grid";
|
||||
|
||||
export const IntegrationsPageContent = () => {
|
||||
const { selectedMailbox } = useMailboxContext();
|
||||
|
||||
if (!selectedMailbox) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page__content">
|
||||
<IntegrationsDataGrid mailbox={selectedMailbox} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
@use "sass:map";
|
||||
@use "../../../../../styles/cunningham-tokens" as *;
|
||||
|
||||
.modal-compose-integration {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
// Channel Type Selector
|
||||
.channel-type-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--c--globals--spacings--xl);
|
||||
}
|
||||
|
||||
.channel-type-selector__subtitle {
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.channel-type-selector__cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--c--globals--spacings--base);
|
||||
}
|
||||
|
||||
.channel-type-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--base);
|
||||
padding: var(--c--globals--spacings--xl);
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
border: 1px solid var(--c--contextuals--border--semantic--neutral--default);
|
||||
border-radius: var(--c--globals--spacings--sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover:not(.channel-type-card--disabled) {
|
||||
border-color: var(--c--contextuals--border--semantic--brand--default);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:focus-visible:not(.channel-type-card--disabled) {
|
||||
outline: 2px solid var(--c--contextuals--border--semantic--brand--default);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active:not(.channel-type-card--disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: var(--c--contextuals--background--surface--secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.channel-type-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: var(--c--contextuals--background--surface--brand--soft);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
color: var(--c--contextuals--content--semantic--brand--primary);
|
||||
}
|
||||
|
||||
.channel-type-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--c--globals--spacings--xs);
|
||||
}
|
||||
|
||||
.channel-type-card__title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
}
|
||||
|
||||
.channel-type-card__description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.channel-type-card__badge {
|
||||
position: absolute;
|
||||
top: var(--c--globals--spacings--sm);
|
||||
right: var(--c--globals--spacings--sm);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 4px 10px;
|
||||
background: var(--c--contextuals--background--surface--tertiary);
|
||||
border-radius: 100px;
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
}
|
||||
|
||||
// Widget Integration Form
|
||||
.widget-integration-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--c--globals--spacings--lg);
|
||||
}
|
||||
|
||||
.widget-integration-form__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--base);
|
||||
}
|
||||
|
||||
.widget-integration-form__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--c--globals--spacings--sm);
|
||||
|
||||
h3 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
}
|
||||
|
||||
&--info {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: var(--c--globals--spacings--base);
|
||||
background: var(--c--contextuals--background--surface--brand--soft);
|
||||
border-radius: var(--c--globals--spacings--xs);
|
||||
|
||||
.material-icons {
|
||||
color: var(--c--contextuals--content--semantic--brand--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget-integration-form__section-description {
|
||||
font-size: 0.85rem;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.widget-integration-form__snippet {
|
||||
position: relative;
|
||||
background: var(--c--contextuals--background--surface--tertiary);
|
||||
border: 1px solid var(--c--contextuals--border--semantic--neutral--default);
|
||||
border-radius: var(--c--globals--spacings--xs);
|
||||
padding: var(--c--globals--spacings--base);
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Fira Code", "Consolas", monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: var(--c--globals--spacings--xs);
|
||||
right: var(--c--globals--spacings--xs);
|
||||
}
|
||||
}
|
||||
|
||||
.widget-integration-form__doc-link {
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--xs);
|
||||
color: var(--c--contextuals--content--semantic--brand--primary);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget-integration-form__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--c--globals--spacings--sm);
|
||||
padding-top: var(--c--globals--spacings--base);
|
||||
border-top: 1px solid var(--c--contextuals--border--semantic--neutral--default);
|
||||
}
|
||||
|
||||
// Tags Selector (reuses thread-labels-widget and cunningham styles)
|
||||
.tags-selector {
|
||||
position: relative;
|
||||
width: 100% !important;
|
||||
|
||||
&--loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--xs);
|
||||
}
|
||||
}
|
||||
|
||||
.tags-selector__wrapper {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
border-radius: var(--c--components--forms-select--border-radius);
|
||||
border: var(--c--components--forms-select--border-width) var(--c--components--forms-select--border-style) var(--c--components--forms-select--border-color);
|
||||
background-color: var(--c--components--forms-select--background-color);
|
||||
min-height: var(--c--components--forms-select--height);
|
||||
padding: 0 var(--c--globals--spacings--s);
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
transition: border var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out),
|
||||
box-shadow var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out),
|
||||
border-radius var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);
|
||||
|
||||
&:hover {
|
||||
border-radius: var(--c--components--forms-select--border-radius--hover);
|
||||
border-color: var(--c--components--forms-select--border-color--hover);
|
||||
box-shadow: var(--c--components--forms-select--border-color--hover) 0 0 0 1px;
|
||||
}
|
||||
|
||||
.c__labelled-box {
|
||||
min-height: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-selector__value {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--c--globals--spacings--xs);
|
||||
flex: 1;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.tags-selector__popup {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
import { Modal, ModalSize, Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { Icon, IconType, IconSize } from "@gouvfr-lasuite/ui-kit";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Channel } from "@/features/api/gen";
|
||||
import { WidgetIntegrationForm } from "./widget-integration-form";
|
||||
import { useConfig } from "@/features/providers/config";
|
||||
|
||||
type ModalComposeIntegrationProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
channel?: Channel;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
type ChannelType = "widget" | "api_key";
|
||||
type ViewState = "select_type" | "form";
|
||||
|
||||
type ChannelTypeCardProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
type ChannelTypeMetadata = {
|
||||
type: ChannelType;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const CHANNEL_TYPE_METADATA: Record<ChannelType, ChannelTypeMetadata> = {
|
||||
widget: {
|
||||
type: "widget",
|
||||
title: "Website Widget",
|
||||
description: "Add a contact form widget to your website to receive messages directly in your mailbox.",
|
||||
icon: "widgets",
|
||||
},
|
||||
api_key: {
|
||||
type: "api_key",
|
||||
title: "API Key",
|
||||
description: "Generate an API key to send messages programmatically from your applications.",
|
||||
icon: "key",
|
||||
disabled: true
|
||||
},
|
||||
};
|
||||
|
||||
const ChannelTypeCard = ({ title, description, icon, disabled, onClick }: ChannelTypeCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`channel-type-card ${disabled ? "channel-type-card--disabled" : ""}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{disabled && (
|
||||
<span className="channel-type-card__badge">{t("Coming soon")}</span>
|
||||
)}
|
||||
<div className="channel-type-card__icon">
|
||||
<Icon name={icon} type={IconType.OUTLINED} size={IconSize.LARGE} />
|
||||
</div>
|
||||
<div className="channel-type-card__content">
|
||||
<h3 className="channel-type-card__title">{title}</h3>
|
||||
<p className="channel-type-card__description">{description}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const BackButton = ({ onClick }: { onClick: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
icon={<Icon name="arrow_back" type={IconType.OUTLINED} />}
|
||||
onClick={onClick}
|
||||
aria-label={t("Back")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalComposeIntegration = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
channel: initialChannel,
|
||||
onSuccess,
|
||||
}: ModalComposeIntegrationProps) => {
|
||||
const { t } = useTranslation();
|
||||
const config = useConfig();
|
||||
const [currentChannel, setCurrentChannel] = useState<Channel | undefined>(initialChannel);
|
||||
const isEditing = !!currentChannel;
|
||||
const [viewState, setViewState] = useState<ViewState>(isEditing ? "form" : "select_type");
|
||||
const [selectedType, setSelectedType] = useState<ChannelType | null>(
|
||||
currentChannel?.type as ChannelType | null
|
||||
);
|
||||
|
||||
const enabledChannelTypes = (config.FEATURE_MAILBOX_ADMIN_CHANNELS || []) as string[];
|
||||
|
||||
// Reset state when modal opens/closes or channel changes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (initialChannel) {
|
||||
setCurrentChannel(initialChannel);
|
||||
setViewState("form");
|
||||
setSelectedType(initialChannel.type as ChannelType);
|
||||
} else {
|
||||
setCurrentChannel(undefined);
|
||||
setViewState("select_type");
|
||||
setSelectedType(null);
|
||||
}
|
||||
}
|
||||
}, [isOpen, initialChannel]);
|
||||
|
||||
const handleSelectType = (type: ChannelType) => {
|
||||
setSelectedType(type);
|
||||
setViewState("form");
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setViewState("select_type");
|
||||
setSelectedType(null);
|
||||
};
|
||||
|
||||
const handleSuccess = (newChannel: Channel) => {
|
||||
// When a new channel is created, switch to edit mode
|
||||
setCurrentChannel(newChannel);
|
||||
onSuccess?.();
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
if (viewState === "select_type") {
|
||||
return t("Create a new integration");
|
||||
}
|
||||
if (selectedType === "widget") {
|
||||
return isEditing ? t("Edit Widget") : t("Create a Widget");
|
||||
}
|
||||
return t("Integrations");
|
||||
};
|
||||
|
||||
// Show back button only when in form view after selecting a type (not when editing existing)
|
||||
const showBackButton = viewState === "form" && !isEditing;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={getTitle()}
|
||||
size={ModalSize.LARGE}
|
||||
leftActions={showBackButton ? <BackButton onClick={handleBack} /> : undefined}
|
||||
>
|
||||
<div className="modal-compose-integration">
|
||||
{viewState === "select_type" && (
|
||||
<div className="channel-type-selector">
|
||||
<p className="channel-type-selector__subtitle">
|
||||
{t("Choose the type of integration you want to create")}
|
||||
</p>
|
||||
<div className="channel-type-selector__cards">
|
||||
{enabledChannelTypes.map((channelType) => {
|
||||
const metadata = CHANNEL_TYPE_METADATA[channelType as ChannelType];
|
||||
if (!metadata) return null;
|
||||
|
||||
return (
|
||||
<ChannelTypeCard
|
||||
key={channelType}
|
||||
title={t(metadata.title)}
|
||||
description={t(metadata.description)}
|
||||
icon={metadata.icon}
|
||||
onClick={() => handleSelectType(metadata.type)}
|
||||
disabled={metadata.disabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{viewState === "form" && selectedType === "widget" && (
|
||||
<WidgetIntegrationForm
|
||||
channel={currentChannel}
|
||||
onSuccess={handleSuccess}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
import { TreeLabel, ThreadLabel, useLabelsList } from "@/features/api/gen";
|
||||
import { Icon, IconType, IconSize, Spinner } from "@gouvfr-lasuite/ui-kit";
|
||||
import { Button, Checkbox, Field, Input, LabelledBox, useModal } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMailboxContext } from "@/features/providers/mailbox";
|
||||
import StringHelper from "@/features/utils/string-helper";
|
||||
import { LabelModal } from "@/features/layouts/components/mailbox-panel/components/mailbox-labels/components/label-form-modal";
|
||||
import { Badge } from "@/features/ui/components/badge";
|
||||
import { ColorHelper } from "@/features/utils/color-helper";
|
||||
|
||||
type TagsSelectorProps = {
|
||||
selectedTags: string[];
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
};
|
||||
|
||||
// Convert TreeLabel to ThreadLabel format for display
|
||||
const treeToThreadLabel = (label: TreeLabel): ThreadLabel => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
slug: label.slug,
|
||||
color: label.color ?? undefined,
|
||||
display_name: label.display_name,
|
||||
description: label.description ?? undefined,
|
||||
is_auto: label.is_auto,
|
||||
});
|
||||
|
||||
// Flatten tree labels into a list with all nested children
|
||||
const flattenLabels = (labels: TreeLabel[]): TreeLabel[] => {
|
||||
const result: TreeLabel[] = [];
|
||||
const flatten = (label: TreeLabel) => {
|
||||
result.push(label);
|
||||
label.children.forEach(flatten);
|
||||
};
|
||||
labels.forEach(flatten);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const TagsSelector = ({ selectedTags, onTagsChange }: TagsSelectorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedMailbox, invalidateLabels } = useMailboxContext();
|
||||
const { open, close, isOpen } = useModal();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: labelsList, isLoading } = useLabelsList(
|
||||
{ mailbox_id: selectedMailbox?.id ?? '' },
|
||||
{ query: { enabled: !!selectedMailbox } }
|
||||
);
|
||||
|
||||
const allLabels = useMemo(() => flattenLabels(labelsList?.data || []), [labelsList?.data]);
|
||||
|
||||
const selectedLabelsAsThreadLabels = useMemo(() => {
|
||||
return allLabels
|
||||
.filter((label) => selectedTags.includes(label.id))
|
||||
.map(treeToThreadLabel);
|
||||
}, [allLabels, selectedTags]);
|
||||
|
||||
const labelsOptions = useMemo(() => {
|
||||
return allLabels
|
||||
.map((label) => ({
|
||||
...treeToThreadLabel(label),
|
||||
checked: selectedTags.includes(label.id),
|
||||
}))
|
||||
.filter((option) => {
|
||||
const normalizedLabel = StringHelper.normalizeForSearch(option.name);
|
||||
const normalizedSearchQuery = StringHelper.normalizeForSearch(searchQuery);
|
||||
return normalizedLabel.includes(normalizedSearchQuery);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.checked !== b.checked) return a.checked ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [allLabels, selectedTags, searchQuery]);
|
||||
|
||||
const handleToggleTag = (tagId: string) => {
|
||||
if (selectedTags.includes(tagId)) {
|
||||
onTagsChange(selectedTags.filter((id) => id !== tagId));
|
||||
} else {
|
||||
onTagsChange([...selectedTags, tagId]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tagId: string) => {
|
||||
onTagsChange(selectedTags.filter((id) => id !== tagId));
|
||||
};
|
||||
|
||||
const handleCreateLabel = (label: { id: string }) => {
|
||||
onTagsChange([...selectedTags, label.id]);
|
||||
invalidateLabels();
|
||||
};
|
||||
|
||||
const showLabelAsPlaceholder = selectedLabelsAsThreadLabels.length === 0;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="tags-selector tags-selector--loading">
|
||||
<Spinner size="sm" />
|
||||
<span>{t('Loading tags...')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Field
|
||||
className="tags-selector"
|
||||
text={t('These tags will be automatically applied to every incoming message from the widget.')}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="tags-selector__wrapper"
|
||||
onClick={() => setIsPopupOpen(true)}
|
||||
>
|
||||
<LabelledBox
|
||||
label={t('Tags')}
|
||||
labelAsPlaceholder={showLabelAsPlaceholder}
|
||||
>
|
||||
<div className="tags-selector__value">
|
||||
{selectedLabelsAsThreadLabels.map((label) => {
|
||||
const badgeColor = label.color
|
||||
? ColorHelper.getContrastColor(label.color, {
|
||||
lightColor: '#fff',
|
||||
darkColor: '#000'
|
||||
})
|
||||
: undefined;
|
||||
return (
|
||||
<Badge
|
||||
key={label.id}
|
||||
className="label-badge label-badge--compact"
|
||||
style={label.color ? { backgroundColor: label.color, color: badgeColor } : undefined}
|
||||
>
|
||||
<span className="label-badge__label">{label.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="label-badge__remove-cta"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveTag(label.id);
|
||||
}}
|
||||
aria-label={t('Remove tag')}
|
||||
>
|
||||
<Icon name="close" size={IconSize.SMALL} type={IconType.OUTLINED} />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="c__select__inner__actions">
|
||||
<Button
|
||||
type="button"
|
||||
variant="tertiary"
|
||||
size="nano"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsPopupOpen(true);
|
||||
}}
|
||||
icon={<Icon name="new_label" type={IconType.OUTLINED} />}
|
||||
aria-label={t('Add tags')}
|
||||
/>
|
||||
</div>
|
||||
</LabelledBox>
|
||||
</div>
|
||||
|
||||
{isPopupOpen && (
|
||||
<>
|
||||
<div className="thread-labels-widget__popup tags-selector__popup">
|
||||
<header className="thread-labels-widget__popup__header">
|
||||
<h3><Icon type={IconType.OUTLINED} name="new_label" /> {t('Add tags')}</h3>
|
||||
<Input
|
||||
className="thread-labels-widget__popup__search"
|
||||
type="search"
|
||||
icon={<Icon type={IconType.OUTLINED} name="search" />}
|
||||
label={t('Search a tag')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</header>
|
||||
<ul className="thread-labels-widget__popup__content">
|
||||
{labelsOptions.map((option) => (
|
||||
<li key={option.id}>
|
||||
<Checkbox
|
||||
checked={option.checked}
|
||||
onChange={() => handleToggleTag(option.id)}
|
||||
label={option.name}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
<li className="thread-labels-widget__popup__content__empty">
|
||||
<Button
|
||||
type="button"
|
||||
color="brand"
|
||||
variant="primary"
|
||||
onClick={open}
|
||||
fullWidth
|
||||
icon={<Icon type={IconType.OUTLINED} name="add" />}
|
||||
>
|
||||
<span className="thread-labels-widget__popup__content__empty__button-label">
|
||||
{searchQuery && labelsOptions.length === 0
|
||||
? t('Create the label "{{label}}"', { label: searchQuery })
|
||||
: t('Create a new label')}
|
||||
</span>
|
||||
</Button>
|
||||
<LabelModal
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
label={{ display_name: searchQuery }}
|
||||
onSuccess={handleCreateLabel}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="thread-labels-widget__popup__overlay" onClick={() => setIsPopupOpen(false)}></div>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { Icon, IconType, IconSize } from "@gouvfr-lasuite/ui-kit";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Channel,
|
||||
useMailboxesChannelsCreate,
|
||||
useMailboxesChannelsPartialUpdate,
|
||||
getMailboxesChannelsListUrl,
|
||||
} from "@/features/api/gen";
|
||||
import { useMailboxContext } from "@/features/providers/mailbox";
|
||||
import { RhfInput } from "@/features/forms/components/react-hook-form";
|
||||
import { addToast, ToasterItem } from "@/features/ui/components/toaster";
|
||||
import { Banner } from "@/features/ui/components/banner";
|
||||
import { handle } from "@/features/utils/errors";
|
||||
import { TagsSelector } from "./tags-selector";
|
||||
|
||||
type WidgetChannelSettings = {
|
||||
tags?: string[];
|
||||
subject_template?: string;
|
||||
config?: { enabled: boolean };
|
||||
};
|
||||
|
||||
type WidgetIntegrationFormProps = {
|
||||
channel?: Channel;
|
||||
onSuccess: (channel: Channel) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const createFormSchema = (t: (key: string) => string) => z.object({
|
||||
name: z.string().min(1, { message: t("Name is required.") }),
|
||||
subject_template: z.string().min(1, { message: t("Subject template is required.") }),
|
||||
});
|
||||
|
||||
type FormFields = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
|
||||
export const WidgetIntegrationForm = ({
|
||||
channel,
|
||||
onSuccess,
|
||||
onClose,
|
||||
}: WidgetIntegrationFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedMailbox } = useMailboxContext();
|
||||
const queryClient = useQueryClient();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const widgetSettings = (channel?.settings as WidgetChannelSettings | undefined);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>(
|
||||
widgetSettings?.tags || []
|
||||
);
|
||||
const isEditing = !!channel;
|
||||
|
||||
const createMutation = useMailboxesChannelsCreate();
|
||||
const updateMutation = useMailboxesChannelsPartialUpdate();
|
||||
|
||||
const formSchema = useMemo(() => createFormSchema(t), [t]);
|
||||
|
||||
const form = useForm<FormFields>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: channel?.name || "",
|
||||
subject_template: widgetSettings?.subject_template || t("Message from {referer_domain}"),
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, formState: { errors } } = form;
|
||||
|
||||
const invalidateChannels = async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [getMailboxesChannelsListUrl(selectedMailbox!.id)],
|
||||
exact: false
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (data: FormFields) => {
|
||||
setError(null);
|
||||
|
||||
const settings = {
|
||||
subject_template: data.subject_template,
|
||||
tags: selectedTags,
|
||||
config: { enabled: true },
|
||||
};
|
||||
|
||||
try {
|
||||
if (isEditing && channel) {
|
||||
// For updates, only send name and settings (not type)
|
||||
await updateMutation.mutateAsync({
|
||||
mailboxId: selectedMailbox!.id,
|
||||
id: channel.id,
|
||||
data: {
|
||||
name: data.name,
|
||||
settings,
|
||||
},
|
||||
});
|
||||
addToast(
|
||||
<ToasterItem type="info">
|
||||
<span>{t("Integration updated!")}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
await invalidateChannels();
|
||||
} else {
|
||||
// For creation, include type
|
||||
const newChannel = await createMutation.mutateAsync({
|
||||
mailboxId: selectedMailbox!.id,
|
||||
data: {
|
||||
name: data.name,
|
||||
type: "widget",
|
||||
settings,
|
||||
},
|
||||
});
|
||||
addToast(
|
||||
<ToasterItem type="info">
|
||||
<span>{t("Integration created!")}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
await invalidateChannels();
|
||||
if (newChannel.status === 201) {
|
||||
onSuccess(newChannel.data);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
handle(err);
|
||||
setError(t("An error occurred while saving the integration."));
|
||||
}
|
||||
};
|
||||
|
||||
const widgetSnippet = channel ? `<script src="${process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_PATH}loader.js" async></script>
|
||||
<script>
|
||||
window._lasuite_widget = window._lasuite_widget || [];
|
||||
_lasuite_widget.push(["loader", "init", {
|
||||
"params": {
|
||||
"api": "${process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_API_URL}",
|
||||
"channel": "${channel.id}"
|
||||
},
|
||||
"script": "${process.env.NEXT_PUBLIC_FEEDBACK_WIDGET_PATH}feedback.js",
|
||||
"widget": "feedback",
|
||||
}]);
|
||||
</script>` : "";
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="widget-integration-form">
|
||||
<div className="widget-integration-form__section">
|
||||
<h3>{t("General")}</h3>
|
||||
<RhfInput
|
||||
label={t("Name")}
|
||||
name="name"
|
||||
text={errors.name?.message || t("This name is for internal use only and will not be visible to users.")}
|
||||
state={errors.name ? "error" : "default"}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="widget-integration-form__section">
|
||||
<h3>{t("Settings")}</h3>
|
||||
<RhfInput
|
||||
label={t("Subject template")}
|
||||
name="subject_template"
|
||||
text={errors.subject_template?.message || t("Use {referer_domain} to include the website domain in the subject.")}
|
||||
state={errors.subject_template ? "error" : "default"}
|
||||
fullWidth
|
||||
/>
|
||||
<TagsSelector
|
||||
selectedTags={selectedTags}
|
||||
onTagsChange={setSelectedTags}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isEditing && (
|
||||
<div className="widget-integration-form__section widget-integration-form__section--info">
|
||||
<Icon name="info" type={IconType.OUTLINED} />
|
||||
<p>
|
||||
{t("After creating the widget, you will receive the installation code to add to your website.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Banner type="error">{error}</Banner>
|
||||
)}
|
||||
|
||||
<div className="widget-integration-form__actions">
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
{isEditing ? t("Save changes") : t("Create integration")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isEditing && channel && (
|
||||
<div className="widget-integration-form__section">
|
||||
<h3>{t("Installation")}</h3>
|
||||
<p className="widget-integration-form__section-description">
|
||||
{t("Add this code snippet to your website to display the feedback widget.")}
|
||||
</p>
|
||||
<div className="widget-integration-form__snippet">
|
||||
<pre><code>{widgetSnippet}</code></pre>
|
||||
<Button
|
||||
type="button"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
icon={<Icon name="content_copy" type={IconType.OUTLINED} />}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(widgetSnippet);
|
||||
addToast(
|
||||
<ToasterItem type="info">
|
||||
<span>{t("Copied to clipboard")}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
} catch {
|
||||
addToast(
|
||||
<ToasterItem type="error">
|
||||
<span>{t("Unable to copy to clipboard.")}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Copy")}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="widget-integration-form__doc-link">
|
||||
<a
|
||||
href="https://integration.lasuite.numerique.gouv.fr/guides/feedback/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("View full documentation")}
|
||||
<Icon name="open_in_new" type={IconType.OUTLINED} size={IconSize.SMALL} />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { SearchInput } from "@/features/forms/components/search-input";
|
||||
import useAbility, { Abilities } from "@/hooks/use-ability";
|
||||
import { useFeatureFlag, FEATURE_KEYS } from "@/hooks/use-feature";
|
||||
import { useAuth, logout } from "@/features/auth";
|
||||
import { LanguagePicker } from "@/features/layouts/components/main/language-picker";
|
||||
import { LagaufreButton } from "@/features/ui/components/lagaufre";
|
||||
@@ -89,6 +90,7 @@ const ApplicationMenu = () => {
|
||||
const canAccessDomainAdmin = useAbility(Abilities.CAN_VIEW_DOMAIN_ADMIN);
|
||||
const canImportMessages = useAbility(Abilities.CAN_IMPORT_MESSAGES, selectedMailbox);
|
||||
const canManageMessageTemplates = useAbility(Abilities.CAN_MANAGE_MESSAGE_TEMPLATES, selectedMailbox);
|
||||
const isIntegrationsEnabled = useFeatureFlag(FEATURE_KEYS.MAILBOX_ADMIN_CHANNELS);
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const taskId = useMemo(() => {
|
||||
@@ -155,6 +157,15 @@ const ApplicationMenu = () => {
|
||||
}
|
||||
}
|
||||
}] : []),
|
||||
...(canManageMessageTemplates && isIntegrationsEnabled ? [{
|
||||
label: t("Integrations"),
|
||||
icon: <Icon name="integration_instructions" type={IconType.OUTLINED} />,
|
||||
callback: () => {
|
||||
if (selectedMailbox) {
|
||||
router.push(`/mailbox/${selectedMailbox.id}/integrations`);
|
||||
}
|
||||
}
|
||||
}] : []),
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -18,6 +18,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
||||
AI_ENABLED: false,
|
||||
FEATURE_AI_SUMMARY: false,
|
||||
FEATURE_AI_AUTOLABELS: false,
|
||||
FEATURE_MAILBOX_ADMIN_CHANNELS: [],
|
||||
SCHEMA_CUSTOM_ATTRIBUTES_USER: {},
|
||||
SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN: {},
|
||||
MAX_OUTGOING_ATTACHMENT_SIZE: 0,
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum FEATURE_KEYS {
|
||||
DRIVE = 'drive',
|
||||
AI_SUMMARY = 'ai_summary',
|
||||
AI_AUTOLABELS = 'ai_autolabels',
|
||||
MAILBOX_ADMIN_CHANNELS = 'mailbox_admin_channels',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,8 @@ export const useFeatureFlag = (featureKey: FEATURE_KEYS) => {
|
||||
return config.AI_ENABLED === true && config.FEATURE_AI_SUMMARY === true;
|
||||
case FEATURE_KEYS.AI_AUTOLABELS:
|
||||
return config.AI_ENABLED === true && config.FEATURE_AI_AUTOLABELS === true;
|
||||
case FEATURE_KEYS.MAILBOX_ADMIN_CHANNELS:
|
||||
return Array.isArray(config.FEATURE_MAILBOX_ADMIN_CHANNELS) && config.FEATURE_MAILBOX_ADMIN_CHANNELS.length > 0;
|
||||
default:
|
||||
throw new Error(`Unknown feature key: ${featureKey}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { MainLayout } from "@/features/layouts/components/main";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMailboxContext } from "@/features/providers/mailbox";
|
||||
import { IntegrationsPageContent } from "@/features/layouts/components/mailbox-settings/integrations-view/page-content";
|
||||
import { CreateIntegrationAction } from "@/features/layouts/components/mailbox-settings/integrations-view/create-integration-action";
|
||||
import { useFeatureFlag, FEATURE_KEYS } from "@/hooks/use-feature";
|
||||
|
||||
const MailboxIntegrationsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { queryStates, selectedMailbox } = useMailboxContext();
|
||||
const isIntegrationsEnabled = useFeatureFlag(FEATURE_KEYS.MAILBOX_ADMIN_CHANNELS);
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryStates.mailboxes.isLoading && !selectedMailbox) {
|
||||
router.push("/");
|
||||
}
|
||||
}, [queryStates.mailboxes.isLoading, selectedMailbox, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIntegrationsEnabled) {
|
||||
router.push("/");
|
||||
}
|
||||
}, [isIntegrationsEnabled, router]);
|
||||
|
||||
if (!isIntegrationsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page__header">
|
||||
<h1 className="title">{t("Integrations")}</h1>
|
||||
<div className="admin-page__actions">
|
||||
<CreateIntegrationAction />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-page__content">
|
||||
{selectedMailbox && <IntegrationsPageContent />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MailboxIntegrationsPage.getLayout = (page: React.ReactElement) => {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default MailboxIntegrationsPage;
|
||||
@@ -45,6 +45,7 @@
|
||||
@use "./../features/forms/components/search-input";
|
||||
@use "./../features/forms/components/search-filters-form";
|
||||
@use "./../features/controlled-modals/message-importer";
|
||||
@use "./../features/layouts/components/mailbox-settings/modal-compose-integration";
|
||||
@use "./../features/layouts/components/admin/mailbox-credentials";
|
||||
@use "./../features/layouts/components/admin/modal-create-update-mailbox";
|
||||
@use "./../features/layouts/components/admin/modal-create-domain";
|
||||
|
||||
Reference in New Issue
Block a user