Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 914e2f7a57 | |||
| 193f87841e | |||
| b7d13cf81a | |||
| 009103b20a | |||
| 3103cc6674 | |||
| 9965f48ba7 | |||
| d15d7549f7 | |||
| 8fcffd2338 | |||
| 07e937cd8e | |||
| 1f1b421735 | |||
| 5a43ec8411 | |||
| c86e7e02bc | |||
| d91d7a2f60 | |||
| b3b285b1e5 | |||
| 11debee402 | |||
| 1608e4f122 | |||
| edeeee1227 |
+3
-3
@@ -69,14 +69,14 @@ chmod +x setup.sh
|
||||
docker compose -f docker-compose-local.yml up
|
||||
```
|
||||
|
||||
5. Start web apps:
|
||||
4. Start web apps:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
6. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
|
||||
7. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step
|
||||
5. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
|
||||
6. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step
|
||||
|
||||
That’s it! You’re all set to begin coding. Remember to refresh your browser if changes don’t auto-reload. Happy contributing! 🎉
|
||||
|
||||
|
||||
@@ -341,7 +341,10 @@ class ProjectViewSet(BaseViewSet):
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
{
|
||||
"name": "The project name is already taken",
|
||||
"code": "PROJECT_NAME_ALREADY_EXIST",
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
@@ -350,7 +353,10 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
except serializers.ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
{
|
||||
"identifier": "The project identifier is already taken",
|
||||
"code": "PROJECT_IDENTIFIER_ALREADY_EXIST",
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
@@ -419,7 +425,10 @@ class ProjectViewSet(BaseViewSet):
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
{
|
||||
"name": "The project name is already taken",
|
||||
"code": "PROJECT_NAME_ALREADY_EXIST",
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||
@@ -428,7 +437,10 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
except serializers.ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
{
|
||||
"identifier": "The project identifier is already taken",
|
||||
"code": "PROJECT_IDENTIFIER_ALREADY_EXIST",
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ def send_email_notification(
|
||||
"project": str(issue.project.name),
|
||||
"user_preference": f"{base_api}/profile/preferences/email",
|
||||
"comments": comments,
|
||||
"entity_type": "issue",
|
||||
}
|
||||
html_content = render_to_string(
|
||||
"emails/notifications/issue-updates.html", context
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.21 on 2025-06-06 12:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0096_user_is_email_valid_user_masked_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -122,6 +122,9 @@ class Project(BaseModel):
|
||||
# timezone
|
||||
TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
timezone = models.CharField(max_length=255, default="UTC", choices=TIMEZONE_CHOICES)
|
||||
# external_id for imports
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
@property
|
||||
def cover_image_url(self):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# base requirements
|
||||
|
||||
# django
|
||||
Django==4.2.21
|
||||
Django==4.2.22
|
||||
# rest framework
|
||||
djangorestframework==3.15.2
|
||||
# postgres
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Updates on issue</title>
|
||||
<title>Updates on {{entity_type}}</title>
|
||||
<style type="text/css" emogrify="no"> html { font-family: system-ui; } p, h1, h2, h3, h4, ol, ul { margin: 0; } h-full { height: 100%; } a:hover { color: #3358d4 !important; } </style>
|
||||
<style> *[class="gmail-fix"] { display: none !important; } </style>
|
||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
|
||||
@@ -37,7 +37,7 @@
|
||||
{% else %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> {{summary}} <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %} </span>and others. </p>
|
||||
{% endif %} <!-- {% if actors_involved == 1 %} {% if data|length > 0 and comments|length == 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name }} </span> made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} to the issue. </p> {% elif data|length == 0 and comments|length > 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name }} </span> added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %}. </p> {% elif data|length > 0 and comments|length > 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name }} </span> made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} and added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %} on the issue. </p> {% endif %} {% else %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> There are {{ total_updates }} new updates and {{total_comments}} new comments on the issue. </p> {% endif %} --> {% for update in data %} {% if update.changes.name %} <!-- Issue title updated -->
|
||||
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c"> The issue title has been updated to {{ issue.name}} </p>
|
||||
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c"> The {{entity_type}} title has been updated to {{ issue.name}} </p>
|
||||
{% endif %} <!-- Outer update Box start --> {% if data %}
|
||||
<div style=" background-color: #f7f9ff; border-radius: 8px; border-style: solid; border-width: 1px; border-color: #c1d0ff; padding: 20px; margin-top: 15px; max-width: 100%; " >
|
||||
<!-- Block Heading -->
|
||||
@@ -224,7 +224,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ issue_url }}" style="text-decoration: none;">
|
||||
<div style=" max-width: min-content; white-space: nowrap; background-color: #3e63dd; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: white; " > View issue </div>
|
||||
<div style=" max-width: min-content; white-space: nowrap; background-color: #3e63dd; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: white; " > View {{entity_type}} </div>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
@@ -232,7 +232,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-size: 0.8rem; color: #1c2024">
|
||||
This email was sent to <a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none" >{{ receiver.email }}.</a > If you'd rather not receive this kind of email, <a href="{{ issue_url }}" style="color: #3a5bc7; text-decoration: none" >you can unsubscribe to the issue</a > or <a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none" >manage your email preferences</a >. <!-- Github | LinkedIn | Twitter -->
|
||||
This email was sent to <a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none" >{{ receiver.email }}.</a > If you'd rather not receive this kind of email, <a href="{{ issue_url }}" style="color: #3a5bc7; text-decoration: none" >you can unsubscribe to the {{entity_type}}</a > or <a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none" >manage your email preferences</a >. <!-- Github | LinkedIn | Twitter -->
|
||||
<div style="margin-top: 60px; float: right"> <a href="https://github.com/makeplane" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> <a href="https://twitter.com/planepowers" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> </div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
+3
-2
@@ -24,14 +24,15 @@
|
||||
"devDependencies": {
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"turbo": "^2.5.3"
|
||||
"turbo": "^2.5.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"nanoid": "3.3.8",
|
||||
"esbuild": "0.25.0",
|
||||
"@babel/helpers": "7.26.10",
|
||||
"@babel/runtime": "7.26.10",
|
||||
"chokidar": "3.6.0"
|
||||
"chokidar": "3.6.0",
|
||||
"tar-fs": "3.0.9"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Nepodařilo se odstranit projekt z oblíbených. Zkuste to prosím znovu.",
|
||||
"project_created_successfully": "Projekt úspěšně vytvořen",
|
||||
"project_created_successfully_description": "Projekt byl úspěšně vytvořen. Nyní můžete začít přidávat pracovní položky.",
|
||||
"project_name_already_taken": "Název projektu už je zabraný.",
|
||||
"project_identifier_already_taken": "Identifikátor projektu už je zabraný.",
|
||||
"project_cover_image_alt": "Úvodní obrázek projektu",
|
||||
"name_is_required": "Název je povinný",
|
||||
"title_should_be_less_than_255_characters": "Název by měl být kratší než 255 znaků",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Projekt konnte nicht aus den Favoriten entfernt werden. Bitte versuchen Sie es erneut.",
|
||||
"project_created_successfully": "Projekt erfolgreich erstellt",
|
||||
"project_created_successfully_description": "Das Projekt wurde erfolgreich erstellt. Sie können nun Arbeitselemente hinzufügen.",
|
||||
"project_name_already_taken": "Der Projektname ist bereits vergeben.",
|
||||
"project_identifier_already_taken": "Der Projekt-Identifier ist bereits vergeben.",
|
||||
"project_cover_image_alt": "Titelbild des Projekts",
|
||||
"name_is_required": "Name ist erforderlich",
|
||||
"title_should_be_less_than_255_characters": "Der Titel sollte weniger als 255 Zeichen enthalten",
|
||||
@@ -2466,4 +2468,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane ist nicht gestartet. Dies könnte daran liegen, dass einer oder mehrere Plane-Services nicht starten konnten.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wählen Sie View Logs aus setup.sh und Docker-Logs, um sicherzugehen."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Couldn't remove the project from favorites. Please try again.",
|
||||
"project_created_successfully": "Project created successfully",
|
||||
"project_created_successfully_description": "Project created successfully. You can now start adding work items to it.",
|
||||
"project_name_already_taken": "The project name is already taken.",
|
||||
"project_identifier_already_taken": "The project identifier is already taken.",
|
||||
"project_cover_image_alt": "Project cover image",
|
||||
"name_is_required": "Name is required",
|
||||
"title_should_be_less_than_255_characters": "Title should be less than 255 characters",
|
||||
|
||||
@@ -318,6 +318,8 @@
|
||||
"failed_to_remove_project_from_favorites": "No se pudo eliminar el proyecto de favoritos. Por favor, inténtalo de nuevo.",
|
||||
"project_created_successfully": "Proyecto creado exitosamente",
|
||||
"project_created_successfully_description": "Proyecto creado exitosamente. Ahora puedes comenzar a agregar elementos de trabajo.",
|
||||
"project_name_already_taken": "El nombre del proyecto ya está en uso.",
|
||||
"project_identifier_already_taken": "El identificador del proyecto ya está en uso.",
|
||||
"project_cover_image_alt": "Imagen de portada del proyecto",
|
||||
"name_is_required": "El nombre es requerido",
|
||||
"title_should_be_less_than_255_characters": "El título debe tener menos de 255 caracteres",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Impossible de supprimer le projet des favoris. Veuillez réessayer.",
|
||||
"project_created_successfully": "Projet créé avec succès",
|
||||
"project_created_successfully_description": "Projet créé avec succès. Vous pouvez maintenant commencer à ajouter des éléments de travail.",
|
||||
"project_name_already_taken": "Le nom du projet est déjà pris.",
|
||||
"project_identifier_already_taken": "L’identifiant du projet est déjà pris.",
|
||||
"project_cover_image_alt": "Image de couverture du projet",
|
||||
"name_is_required": "Le nom est requis",
|
||||
"title_should_be_less_than_255_characters": "Le titre doit faire moins de 255 caractères",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Tidak dapat menghapus proyek dari favorit. Silakan coba lagi.",
|
||||
"project_created_successfully": "Proyek berhasil dibuat",
|
||||
"project_created_successfully_description": "Proyek berhasil dibuat. Anda sekarang dapat mulai menambahkan item kerja ke dalamnya.",
|
||||
"project_name_already_taken": "Nama project sudah digunakan.",
|
||||
"project_identifier_already_taken": "Identifier project sudah digunakan.",
|
||||
"project_cover_image_alt": "Gambar sampul proyek",
|
||||
"name_is_required": "Nama diperlukan",
|
||||
"title_should_be_less_than_255_characters": "Judul harus kurang dari 255 karakter",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Impossibile rimuovere il progetto dai preferiti. Per favore, riprova.",
|
||||
"project_created_successfully": "Progetto creato con successo",
|
||||
"project_created_successfully_description": "Progetto creato con successo. Ora puoi iniziare ad aggiungere elementi di lavoro.",
|
||||
"project_name_already_taken": "Il nome del progetto è già stato utilizzato.",
|
||||
"project_identifier_already_taken": "L'identificatore del progetto è già stato utilizzato.",
|
||||
"project_cover_image_alt": "Immagine di copertina del progetto",
|
||||
"name_is_required": "Il nome è obbligatorio",
|
||||
"title_should_be_less_than_255_characters": "Il titolo deve contenere meno di 255 caratteri",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "プロジェクトをお気に入りから削除できませんでした。もう一度お試しください。",
|
||||
"project_created_successfully": "プロジェクトが正常に作成されました",
|
||||
"project_created_successfully_description": "プロジェクトが正常に作成されました。作業項目を追加できるようになりました。",
|
||||
"project_name_already_taken": "プロジェクト名は既に使用されています。",
|
||||
"project_identifier_already_taken": "プロジェクト識別子は既に使用されています。",
|
||||
"project_cover_image_alt": "プロジェクトのカバー画像",
|
||||
"name_is_required": "名前は必須です",
|
||||
"title_should_be_less_than_255_characters": "タイトルは255文字未満である必要があります",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "프로젝트를 즐겨찾기에서 제거하지 못했습니다. 다시 시도해주세요.",
|
||||
"project_created_successfully": "프로젝트가 성공적으로 생성되었습니다",
|
||||
"project_created_successfully_description": "프로젝트가 성공적으로 생성되었습니다. 이제 작업 항목을 추가할 수 있습니다.",
|
||||
"project_name_already_taken": "프로젝트 이름이 이미 사용 중입니다.",
|
||||
"project_identifier_already_taken": "프로젝트 식별자가 이미 사용 중입니다.",
|
||||
"project_cover_image_alt": "프로젝트 커버 이미지",
|
||||
"name_is_required": "이름이 필요합니다",
|
||||
"title_should_be_less_than_255_characters": "제목은 255자 미만이어야 합니다",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Nie udało się usunąć projektu z ulubionych. Spróbuj ponownie.",
|
||||
"project_created_successfully": "Projekt utworzono pomyślnie",
|
||||
"project_created_successfully_description": "Projekt został pomyślnie utworzony. Teraz możesz dodawać elementy pracy.",
|
||||
"project_name_already_taken": "Nazwa projektu jest już zajęta.",
|
||||
"project_identifier_already_taken": "Identyfikator projektu jest już zajęty.",
|
||||
"project_cover_image_alt": "Obraz w tle projektu",
|
||||
"name_is_required": "Nazwa jest wymagana",
|
||||
"title_should_be_less_than_255_characters": "Nazwa musi mieć mniej niż 255 znaków",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Não foi possível remover o projeto dos favoritos. Por favor, tente novamente.",
|
||||
"project_created_successfully": "Projeto criado com sucesso",
|
||||
"project_created_successfully_description": "Projeto criado com sucesso. Agora você pode começar a adicionar itens de trabalho a ele.",
|
||||
"project_name_already_taken": "O nome do projeto já está em uso.",
|
||||
"project_identifier_already_taken": "O identificador do projeto já está em uso.",
|
||||
"project_cover_image_alt": "Imagem de capa do projeto",
|
||||
"name_is_required": "Nome é obrigatório",
|
||||
"title_should_be_less_than_255_characters": "O título deve ter menos de 255 caracteres",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Nu s-a putut elimina proiectul din favorite. Încearcă din nou.",
|
||||
"project_created_successfully": "Proiect creat cu succes",
|
||||
"project_created_successfully_description": "Proiect creat cu succes. Poți începe să adaugi activități în el.",
|
||||
"project_name_already_taken": "Numele proiectului este deja folosit.",
|
||||
"project_identifier_already_taken": "Identificatorul proiectului este deja folosit.",
|
||||
"project_cover_image_alt": "Coperta proiectului",
|
||||
"name_is_required": "Numele este obligatoriu",
|
||||
"title_should_be_less_than_255_characters": "Titlul trebuie să conțină mai puțin de 255 de caractere",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Не удалось удалить проект из избранного. Попробуйте снова.",
|
||||
"project_created_successfully": "Проект успешно создан",
|
||||
"project_created_successfully_description": "Проект успешно создан. Теперь вы можете добавлять рабочие элементы.",
|
||||
"project_name_already_taken": "Имя проекта уже используется.",
|
||||
"project_identifier_already_taken": "Идентификатор проекта уже используется.",
|
||||
"project_cover_image_alt": "Обложка проекта",
|
||||
"name_is_required": "Требуется имя",
|
||||
"title_should_be_less_than_255_characters": "Заголовок должен быть короче 255 символов",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Nepodarilo sa odstrániť projekt z obľúbených. Skúste to prosím znova.",
|
||||
"project_created_successfully": "Projekt bol úspešne vytvorený",
|
||||
"project_created_successfully_description": "Projekt bol úspešne vytvorený. Teraz môžete začať pridávať pracovné položky.",
|
||||
"project_name_already_taken": "Názov projektu je už použitý.",
|
||||
"project_identifier_already_taken": "Identifikátor projektu je už použitý.",
|
||||
"project_cover_image_alt": "Úvodný obrázok projektu",
|
||||
"name_is_required": "Názov je povinný",
|
||||
"title_should_be_less_than_255_characters": "Názov by mal byť kratší ako 255 znakov",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Proje favorilerden kaldırılamadı. Lütfen tekrar deneyin.",
|
||||
"project_created_successfully": "Proje başarıyla oluşturuldu",
|
||||
"project_created_successfully_description": "Proje başarıyla oluşturuldu. Artık iş öğeleri eklemeye başlayabilirsiniz.",
|
||||
"project_name_already_taken": "Proje ismi zaten kullanılıyor.",
|
||||
"project_identifier_already_taken": "Proje kimliği zaten kullanılıyor.",
|
||||
"project_cover_image_alt": "Proje kapak resmi",
|
||||
"name_is_required": "Ad gereklidir",
|
||||
"title_should_be_less_than_255_characters": "Başlık 255 karakterden az olmalı",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Не вдалося видалити проєкт із вибраного. Спробуйте ще раз.",
|
||||
"project_created_successfully": "Проєкт успішно створено",
|
||||
"project_created_successfully_description": "Проєкт успішно створений. Тепер ви можете почати додавати робочі одиниці.",
|
||||
"project_name_already_taken": "Назва проекту вже використовується.",
|
||||
"project_identifier_already_taken": "Ідентифікатор проекту вже використовується.",
|
||||
"project_cover_image_alt": "Обкладинка проєкту",
|
||||
"name_is_required": "Назва є обов’язковою",
|
||||
"title_should_be_less_than_255_characters": "Назва має бути коротшою за 255 символів",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "Không thể xóa dự án khỏi mục yêu thích. Vui lòng thử lại.",
|
||||
"project_created_successfully": "Dự án đã được tạo thành công",
|
||||
"project_created_successfully_description": "Dự án đã được tạo thành công. Bây giờ bạn có thể bắt đầu thêm mục công việc.",
|
||||
"project_name_already_taken": "Tên project đã được sử dụng.",
|
||||
"project_identifier_already_taken": "Identifier project đã được sử dụng.",
|
||||
"project_cover_image_alt": "Ảnh bìa dự án",
|
||||
"name_is_required": "Tên là bắt buộc",
|
||||
"title_should_be_less_than_255_characters": "Tiêu đề phải ít hơn 255 ký tự",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "无法从收藏中移除项目。请重试。",
|
||||
"project_created_successfully": "项目创建成功",
|
||||
"project_created_successfully_description": "项目创建成功。您现在可以开始添加工作项了。",
|
||||
"project_name_already_taken": "项目名称已被使用。",
|
||||
"project_identifier_already_taken": "项目标识符已被使用。",
|
||||
"project_cover_image_alt": "项目封面图片",
|
||||
"name_is_required": "名称为必填项",
|
||||
"title_should_be_less_than_255_characters": "标题应少于255个字符",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"failed_to_remove_project_from_favorites": "無法從我的最愛移除專案。請再試一次。",
|
||||
"project_created_successfully": "專案建立成功",
|
||||
"project_created_successfully_description": "專案建立成功。您現在可以開始新增工作事項。",
|
||||
"project_name_already_taken": "專案名稱已被使用。",
|
||||
"project_identifier_already_taken": "專案識別碼已被使用。",
|
||||
"project_cover_image_alt": "專案封面圖片",
|
||||
"name_is_required": "名稱為必填",
|
||||
"title_should_be_less_than_255_characters": "標題不應超過 255 個字元",
|
||||
|
||||
Vendored
+8
-1
@@ -1,4 +1,4 @@
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { EIssueServiceType, EIssuesStoreType } from "@plane/constants";
|
||||
import { TIssuePriorities } from "../issues";
|
||||
import { TIssueAttachment } from "./issue_attachment";
|
||||
import { TIssueLink } from "./issue_link";
|
||||
@@ -181,3 +181,10 @@ export type TPublicIssuesResponse = {
|
||||
extra_stats: null;
|
||||
results: TPublicIssueResponseResults;
|
||||
};
|
||||
|
||||
export interface IWorkItemPeekOverview {
|
||||
embedIssue?: boolean;
|
||||
embedRemoveCurrentNotification?: () => void;
|
||||
is_draft?: boolean;
|
||||
storeType?: EIssuesStoreType;
|
||||
}
|
||||
+2
-1
@@ -85,15 +85,16 @@ export interface IProjectMemberLite {
|
||||
export type TProjectMembership = {
|
||||
member: string;
|
||||
role: TUserPermissions | EUserProjectRoles;
|
||||
created_at: string;
|
||||
} & (
|
||||
| {
|
||||
id: string;
|
||||
original_role: EUserProjectRoles;
|
||||
created_at: string;
|
||||
}
|
||||
| {
|
||||
id: null;
|
||||
original_role: null;
|
||||
created_at: null;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ type TUseDropdownKeyDown = {
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen, selectActiveItem?) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
|
||||
if (!isOpen) {
|
||||
event.stopPropagation();
|
||||
onOpen();
|
||||
|
||||
@@ -333,3 +333,59 @@ export const generateDateArray = (startDate: string | Date, endDate: string | Da
|
||||
}
|
||||
return dateArray;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats merged date range display with smart formatting
|
||||
* - Single date: "Jan 24, 2025"
|
||||
* - Same year, same month: "Jan 24 - 28, 2025"
|
||||
* - Same year, different month: "Jan 24 - Feb 6, 2025"
|
||||
* - Different year: "Dec 28, 2024 - Jan 4, 2025"
|
||||
*/
|
||||
export const formatDateRange = (
|
||||
parsedStartDate: Date | null | undefined,
|
||||
parsedEndDate: Date | null | undefined
|
||||
): string => {
|
||||
// If no dates are provided
|
||||
if (!parsedStartDate && !parsedEndDate) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// If only start date is provided
|
||||
if (parsedStartDate && !parsedEndDate) {
|
||||
return format(parsedStartDate, "MMM dd, yyyy");
|
||||
}
|
||||
|
||||
// If only end date is provided
|
||||
if (!parsedStartDate && parsedEndDate) {
|
||||
return format(parsedEndDate, "MMM dd, yyyy");
|
||||
}
|
||||
|
||||
// If both dates are provided
|
||||
if (parsedStartDate && parsedEndDate) {
|
||||
const startYear = parsedStartDate.getFullYear();
|
||||
const startMonth = parsedStartDate.getMonth();
|
||||
const endYear = parsedEndDate.getFullYear();
|
||||
const endMonth = parsedEndDate.getMonth();
|
||||
|
||||
// Same year, same month
|
||||
if (startYear === endYear && startMonth === endMonth) {
|
||||
const startDay = format(parsedStartDate, "dd");
|
||||
const endDay = format(parsedEndDate, "dd");
|
||||
return `${format(parsedStartDate, "MMM")} ${startDay} - ${endDay}, ${startYear}`;
|
||||
}
|
||||
|
||||
// Same year, different month
|
||||
if (startYear === endYear) {
|
||||
const startFormatted = format(parsedStartDate, "MMM dd");
|
||||
const endFormatted = format(parsedEndDate, "MMM dd");
|
||||
return `${startFormatted} - ${endFormatted}, ${startYear}`;
|
||||
}
|
||||
|
||||
// Different year
|
||||
const startFormatted = format(parsedStartDate, "MMM dd, yyyy");
|
||||
const endFormatted = format(parsedEndDate, "MMM dd, yyyy");
|
||||
return `${startFormatted} - ${endFormatted}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"ui": "tui",
|
||||
"globalEnv": [
|
||||
"NODE_ENV",
|
||||
"NEXT_PUBLIC_API_BASE_URL",
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
import { InboxContentRoot } from "@/components/inbox";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
import { NotificationsRoot } from "@/components/workspace-notifications";
|
||||
// hooks
|
||||
import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
const WorkspaceDashboardPage = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
@@ -24,98 +16,15 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const {
|
||||
currentSelectedNotificationId,
|
||||
setCurrentSelectedNotificationId,
|
||||
notificationLiteByNotificationId,
|
||||
notificationIdsByWorkspaceId,
|
||||
getNotifications,
|
||||
} = useWorkspaceNotifications();
|
||||
const { fetchUserProjectInfo } = useUserPermissions();
|
||||
const { setPeekIssue } = useIssueDetail();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name
|
||||
? t("notification.page_label", { workspace: currentWorkspace?.name })
|
||||
: undefined;
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
|
||||
notificationLiteByNotificationId(currentSelectedNotificationId);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" });
|
||||
|
||||
// fetching workspace work item properties
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
|
||||
// fetch workspace notifications
|
||||
const notificationMutation =
|
||||
currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id)
|
||||
? ENotificationLoader.MUTATION_LOADER
|
||||
: ENotificationLoader.INIT_LOADER;
|
||||
const notificationLoader =
|
||||
currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id)
|
||||
? ENotificationQueryParamType.CURRENT
|
||||
: ENotificationQueryParamType.INIT;
|
||||
useSWR(
|
||||
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
|
||||
currentWorkspace?.slug
|
||||
? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
|
||||
: null
|
||||
);
|
||||
|
||||
// fetching user project member info
|
||||
const { isLoading: projectMemberInfoLoader } = useSWR(
|
||||
workspace_slug && project_id && is_inbox_issue
|
||||
? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}`
|
||||
: null,
|
||||
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
|
||||
);
|
||||
|
||||
const embedRemoveCurrentNotification = useCallback(
|
||||
() => setCurrentSelectedNotificationId(undefined),
|
||||
[setCurrentSelectedNotificationId]
|
||||
);
|
||||
|
||||
// clearing up the selected notifications when unmounting the page
|
||||
useEffect(
|
||||
() => () => {
|
||||
setCurrentSelectedNotificationId(undefined);
|
||||
setPeekIssue(undefined);
|
||||
},
|
||||
[setCurrentSelectedNotificationId, setPeekIssue]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
||||
{!currentSelectedNotificationId ? (
|
||||
<div className="w-full h-screen flex justify-center items-center">
|
||||
<SimpleEmptyState title={t("notification.empty_state.detail.title")} assetPath={resolvedPath} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
|
||||
<>
|
||||
{projectMemberInfoLoader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<InboxContentRoot
|
||||
setIsMobileSidebar={() => {}}
|
||||
isMobileSidebar={false}
|
||||
workspaceSlug={workspace_slug}
|
||||
projectId={project_id}
|
||||
inboxIssueId={issue_id}
|
||||
isNotificationEmbed
|
||||
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<IssuePeekOverview embedIssue embedRemoveCurrentNotification={embedRemoveCurrentNotification} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<NotificationsRoot workspaceSlug={workspaceSlug?.toString()} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
|
||||
addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
title: t("toast.error"),
|
||||
message: t("failed_to_remove_project_from_favorites"),
|
||||
});
|
||||
});
|
||||
@@ -89,20 +89,27 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
|
||||
handleNextStep(res.id);
|
||||
})
|
||||
.catch((err) => {
|
||||
Object.keys(err?.data ?? {}).map((key) => {
|
||||
if (err?.data.code === "PROJECT_NAME_ALREADY_EXIST") {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: err.data[key],
|
||||
title: t("toast.error"),
|
||||
message: t("project_name_already_taken"),
|
||||
});
|
||||
captureProjectEvent({
|
||||
eventName: PROJECT_CREATED,
|
||||
payload: {
|
||||
...formData,
|
||||
state: "FAILED",
|
||||
},
|
||||
} else if (err?.data.code === "PROJECT_IDENTIFIER_ALREADY_EXIST") {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("toast.error"),
|
||||
message: t("project_identifier_already_taken"),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.keys(err?.data ?? {}).map((key) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: err.data[key],
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./notification-card/root";
|
||||
export * from "./list-root";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NotificationCardListRoot } from "./notification-card/root";
|
||||
|
||||
export type TNotificationListRoot = {
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const NotificationListRoot = (props: TNotificationListRoot) => <NotificationCardListRoot {...props} />;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { IWorkItemPeekOverview } from "@plane/types";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { TPeekIssue } from "@/store/issue/issue-details/root.store";
|
||||
|
||||
export type TNotificationPreview = {
|
||||
isWorkItem: boolean;
|
||||
PeekOverviewComponent: React.ComponentType<IWorkItemPeekOverview>;
|
||||
setPeekWorkItem: (peekIssue: TPeekIssue | undefined) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function returns if the current active notification is related to work item or an epic.
|
||||
* @returns isWorkItem: boolean, peekOverviewComponent: IWorkItemPeekOverview, setPeekWorkItem
|
||||
*/
|
||||
export const useNotificationPreview = (): TNotificationPreview => {
|
||||
const { peekIssue, setPeekIssue } = useIssueDetail(EIssueServiceType.ISSUES);
|
||||
|
||||
return {
|
||||
isWorkItem: Boolean(peekIssue),
|
||||
PeekOverviewComponent: IssuePeekOverview,
|
||||
setPeekWorkItem: setPeekIssue,
|
||||
};
|
||||
};
|
||||
@@ -3,21 +3,12 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArchiveRestoreIcon,
|
||||
ArrowRight,
|
||||
ChevronRight,
|
||||
EllipsisIcon,
|
||||
LinkIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { ArrowRight, ChevronRight } from "lucide-react";
|
||||
// Plane Imports
|
||||
import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle } from "@plane/types";
|
||||
import { CustomMenu, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// components
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
// helpers
|
||||
@@ -239,6 +230,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
|
||||
</span>
|
||||
}
|
||||
mergeDates
|
||||
showTooltip={!!cycleDetails.start_date && !!cycleDetails.end_date} // show tooltip only if both start and end date are present
|
||||
required={cycleDetails.status !== "draft"}
|
||||
disabled={!isEditingAllowed || isArchived || isCompleted}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
|
||||
import { format, parseISO } from "date-fns";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -23,6 +22,7 @@ import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, s
|
||||
import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { MergedDateDisplay } from "@/components/dropdowns/merged-date";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
@@ -230,9 +230,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
>
|
||||
<div className="flex gap-1 text-xs text-custom-text-300 font-medium items-center">
|
||||
<CalendarDays className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{cycleDetails.start_date && <span>{format(parseISO(cycleDetails.start_date), "MMM dd, yyyy")}</span>}
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{cycleDetails.end_date && <span>{format(parseISO(cycleDetails.end_date), "MMM dd, yyyy")}</span>}
|
||||
<MergedDateDisplay startDate={cycleDetails.start_date} endDate={cycleDetails.end_date} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{projectUTCOffset && (
|
||||
@@ -269,6 +267,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
|
||||
</span>
|
||||
}
|
||||
mergeDates
|
||||
required={cycleDetails.status !== "draft"}
|
||||
disabled
|
||||
hideIcon={{
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { DateRange, Matcher } from "react-day-picker";
|
||||
import { usePopper } from "react-popper";
|
||||
import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react";
|
||||
import { ArrowRight, CalendarCheck2, CalendarDays, X } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
@@ -17,6 +17,7 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
import { MergedDateDisplay } from "./merged-date";
|
||||
// types
|
||||
import { TButtonVariants } from "./types";
|
||||
|
||||
@@ -30,11 +31,14 @@ type Props = {
|
||||
buttonVariant: TButtonVariants;
|
||||
cancelButtonText?: string;
|
||||
className?: string;
|
||||
clearIconClassName?: string;
|
||||
disabled?: boolean;
|
||||
hideIcon?: {
|
||||
from?: boolean;
|
||||
to?: boolean;
|
||||
};
|
||||
isClearable?: boolean;
|
||||
mergeDates?: boolean;
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
onSelect?: (range: DateRange | undefined) => void;
|
||||
@@ -65,11 +69,14 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
buttonToDateClassName,
|
||||
buttonVariant,
|
||||
className,
|
||||
clearIconClassName = "",
|
||||
disabled = false,
|
||||
hideIcon = {
|
||||
from: true,
|
||||
to: true,
|
||||
},
|
||||
isClearable = false,
|
||||
mergeDates,
|
||||
minDate,
|
||||
maxDate,
|
||||
onSelect,
|
||||
@@ -118,20 +125,18 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
setIsOpen,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
setDateRange({
|
||||
from: value.from,
|
||||
to: value.to,
|
||||
});
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const disabledDays: Matcher[] = [];
|
||||
if (minDate) disabledDays.push({ before: minDate });
|
||||
if (maxDate) disabledDays.push({ after: maxDate });
|
||||
|
||||
const clearDates = () => {
|
||||
const clearedRange = { from: undefined, to: undefined };
|
||||
setDateRange(clearedRange);
|
||||
onSelect?.(clearedRange);
|
||||
};
|
||||
|
||||
const hasDisplayedDates = dateRange.from || dateRange.to;
|
||||
|
||||
useEffect(() => {
|
||||
setDateRange(value);
|
||||
}, [value]);
|
||||
@@ -158,9 +163,9 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
tooltipContent={
|
||||
customTooltipContent ?? (
|
||||
<>
|
||||
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
|
||||
{" - "}
|
||||
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
|
||||
{dateRange.from ? renderFormattedDate(dateRange.from) : ""}
|
||||
{dateRange.from && dateRange.to ? " - " : ""}
|
||||
{dateRange.to ? renderFormattedDate(dateRange.to) : ""}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -168,19 +173,70 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
variant={buttonVariant}
|
||||
renderToolTipByDefault={renderByDefault}
|
||||
>
|
||||
<span
|
||||
className={cn("h-full flex items-center justify-center gap-1 rounded-sm flex-grow", buttonFromDateClassName)}
|
||||
>
|
||||
{!hideIcon.from && <CalendarDays className="h-3 w-3 flex-shrink-0" />}
|
||||
{dateRange.from ? renderFormattedDate(dateRange.from) : renderPlaceholder ? placeholder.from : ""}
|
||||
</span>
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0" />
|
||||
<span
|
||||
className={cn("h-full flex items-center justify-center gap-1 rounded-sm flex-grow", buttonToDateClassName)}
|
||||
>
|
||||
{!hideIcon.to && <CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
||||
{dateRange.to ? renderFormattedDate(dateRange.to) : renderPlaceholder ? placeholder.to : ""}
|
||||
</span>
|
||||
{mergeDates ? (
|
||||
// Merged date display
|
||||
<div className="flex items-center gap-1.5 w-full">
|
||||
{!hideIcon.from && <CalendarDays className="h-3 w-3 flex-shrink-0" />}
|
||||
{dateRange.from || dateRange.to ? (
|
||||
<MergedDateDisplay
|
||||
startDate={dateRange.from}
|
||||
endDate={dateRange.to}
|
||||
className="flex-grow truncate text-xs"
|
||||
/>
|
||||
) : (
|
||||
renderPlaceholder && (
|
||||
<>
|
||||
<span className="text-custom-text-400">{placeholder.from}</span>
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0 text-custom-text-400" />
|
||||
<span className="text-custom-text-400">{placeholder.to}</span>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
{isClearable && !disabled && hasDisplayedDates && (
|
||||
<X
|
||||
className={cn("h-2.5 w-2.5 flex-shrink-0 cursor-pointer", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
clearDates();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// Original separate date display
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"h-full flex items-center justify-center gap-1 rounded-sm flex-grow",
|
||||
buttonFromDateClassName
|
||||
)}
|
||||
>
|
||||
{!hideIcon.from && <CalendarDays className="h-3 w-3 flex-shrink-0" />}
|
||||
{dateRange.from ? renderFormattedDate(dateRange.from) : renderPlaceholder ? placeholder.from : ""}
|
||||
</span>
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0" />
|
||||
<span
|
||||
className={cn(
|
||||
"h-full flex items-center justify-center gap-1 rounded-sm flex-grow",
|
||||
buttonToDateClassName
|
||||
)}
|
||||
>
|
||||
{!hideIcon.to && <CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
||||
{dateRange.to ? renderFormattedDate(dateRange.to) : renderPlaceholder ? placeholder.to : ""}
|
||||
</span>
|
||||
{isClearable && !disabled && hasDisplayedDates && (
|
||||
<X
|
||||
className={cn("h-2.5 w-2.5 flex-shrink-0 cursor-pointer ml-1", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
clearDates();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from "./cycle";
|
||||
export * from "./date-range";
|
||||
export * from "./date";
|
||||
export * from "./estimate";
|
||||
export * from "./merged-date";
|
||||
export * from "./module";
|
||||
export * from "./priority";
|
||||
export * from "./project";
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// helpers
|
||||
import { formatDateRange } from "@plane/utils";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
startDate: Date | string | null | undefined;
|
||||
endDate: Date | string | null | undefined;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats merged date range display with smart formatting
|
||||
* - Single date: "Jan 24, 2025"
|
||||
* - Same year, same month: "Jan 24 - 28, 2025"
|
||||
* - Same year, different month: "Jan 24 - Feb 6, 2025"
|
||||
* - Different year: "Dec 28, 2024 - Jan 4, 2025"
|
||||
*/
|
||||
export const MergedDateDisplay: React.FC<Props> = observer((props) => {
|
||||
const { startDate, endDate, className = "" } = props;
|
||||
|
||||
// Parse dates
|
||||
const parsedStartDate = getDate(startDate);
|
||||
const parsedEndDate = getDate(endDate);
|
||||
|
||||
const displayText = formatDateRange(parsedStartDate, parsedEndDate);
|
||||
|
||||
if (!displayText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <span className={className}>{displayText}</span>;
|
||||
});
|
||||
@@ -46,10 +46,7 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("common.link_copied"),
|
||||
message: t("entity.link_copied_to_clipboard", {
|
||||
entity:
|
||||
issueServiceType === EIssueServiceType.ISSUES
|
||||
? t("issue.label", { count: 1 })
|
||||
: t("epic.label", { count: 1 }),
|
||||
entity: t("epic.label", { count: 1 }),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
+42
-24
@@ -1,11 +1,10 @@
|
||||
// plane imports
|
||||
import { SyntheticEvent } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarClock } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||
// components
|
||||
import { PriorityDropdown, MemberDropdown, StateDropdown, DateDropdown } from "@/components/dropdowns";
|
||||
import { PriorityDropdown, MemberDropdown, StateDropdown, DateRangeDropdown } from "@/components/dropdowns";
|
||||
// hooks
|
||||
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/properties/with-display-properties-HOC";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
@@ -37,6 +36,22 @@ export const SubIssuesListItemProperties: React.FC<Props> = observer((props) =>
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleStartDate = (date: Date | null) => {
|
||||
if (issue.project_id) {
|
||||
updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, {
|
||||
start_date: date ? renderFormattedPayloadDate(date) : null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTargetDate = (date: Date | null) => {
|
||||
if (issue.project_id) {
|
||||
updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, {
|
||||
target_date: date ? renderFormattedPayloadDate(date) : null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!displayProperties) return <></>;
|
||||
|
||||
const maxDate = getDate(issue.target_date);
|
||||
@@ -88,29 +103,32 @@ export const SubIssuesListItemProperties: React.FC<Props> = observer((props) =>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
|
||||
<div className="h-5 flex-shrink-0" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
|
||||
<DateDropdown
|
||||
value={issue.target_date ?? null}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
updateSubIssue(
|
||||
workspaceSlug,
|
||||
issue.project_id,
|
||||
parentIssueId,
|
||||
issueId,
|
||||
{
|
||||
target_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
},
|
||||
{ ...issue }
|
||||
)
|
||||
}
|
||||
maxDate={maxDate}
|
||||
placeholder={t("common.order_by.due_date")}
|
||||
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
optionsClassName="z-30"
|
||||
{/* merged dates */}
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey={["start_date", "due_date"]}
|
||||
shouldRenderProperty={(properties) => !!(properties.start_date || properties.due_date)}
|
||||
>
|
||||
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
|
||||
<DateRangeDropdown
|
||||
value={{
|
||||
from: getDate(issue.start_date) || undefined,
|
||||
to: getDate(issue.target_date) || undefined,
|
||||
}}
|
||||
onSelect={(range) => {
|
||||
handleStartDate(range?.from ?? null);
|
||||
handleTargetDate(range?.to ?? null);
|
||||
}}
|
||||
hideIcon={{
|
||||
from: false,
|
||||
}}
|
||||
isClearable
|
||||
mergeDates
|
||||
buttonVariant={issue.start_date || issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
disabled={!disabled}
|
||||
showTooltip
|
||||
customTooltipHeading="Date Range"
|
||||
renderPlaceholder={false}
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
@@ -99,7 +99,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
|
||||
setQuery("");
|
||||
}
|
||||
|
||||
if (query !== "" && e.key === "Enter" && canCreateLabel) {
|
||||
if (query !== "" && e.key === "Enter" && !e.nativeEvent.isComposing && canCreateLabel) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
await handleAddLabel(query);
|
||||
|
||||
@@ -5,7 +5,7 @@ import isNil from "lodash/isNil";
|
||||
import { observer } from "mobx-react";
|
||||
import { Bell, BellOff } from "lucide-react";
|
||||
// plane-i18n
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel, EIssueServiceType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// UI
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
@@ -16,17 +16,18 @@ export type TIssueSubscription = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
serviceType?: EIssueServiceType;
|
||||
};
|
||||
|
||||
export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId } = props;
|
||||
const { workspaceSlug, projectId, issueId, serviceType = EIssueServiceType.ISSUES } = props;
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const {
|
||||
subscription: { getSubscriptionByIssueId },
|
||||
createSubscription,
|
||||
removeSubscription,
|
||||
} = useIssueDetail();
|
||||
} = useIssueDetail(serviceType);
|
||||
// state
|
||||
const [loading, setLoading] = useState(false);
|
||||
// hooks
|
||||
@@ -53,12 +54,12 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
||||
: t("issue.subscription.actions.subscribed"),
|
||||
});
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setLoading(false);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("toast.error"),
|
||||
message: t("commons.error.message"),
|
||||
message: t("common.error.message"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -78,7 +79,7 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
||||
variant="outline-primary"
|
||||
className="hover:!bg-custom-primary-100/20"
|
||||
onClick={handleSubscription}
|
||||
disabled={!isEditable}
|
||||
disabled={!isEditable || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<span>
|
||||
|
||||
@@ -5,7 +5,7 @@ import xor from "lodash/xor";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// icons
|
||||
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
|
||||
import { Layers, Link, Paperclip } from "lucide-react";
|
||||
// types
|
||||
import { ISSUE_UPDATED } from "@plane/constants";
|
||||
// i18n
|
||||
@@ -15,13 +15,13 @@ import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
DateDropdown,
|
||||
EstimateDropdown,
|
||||
PriorityDropdown,
|
||||
MemberDropdown,
|
||||
ModuleDropdown,
|
||||
CycleDropdown,
|
||||
StateDropdown,
|
||||
DateRangeDropdown,
|
||||
} from "@/components/dropdowns";
|
||||
// constants
|
||||
// helpers
|
||||
@@ -265,12 +265,6 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
|
||||
const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
|
||||
|
||||
const minDate = getDate(issue.start_date);
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = getDate(issue.target_date);
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
const handleEventPropagation = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -310,40 +304,34 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
{/* start date */}
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="start_date">
|
||||
{/* merged dates */}
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey={["start_date", "due_date"]}
|
||||
shouldRenderProperty={(properties) => !!(properties.start_date || properties.due_date)}
|
||||
>
|
||||
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
|
||||
<DateDropdown
|
||||
value={issue.start_date ?? null}
|
||||
onChange={handleStartDate}
|
||||
maxDate={maxDate}
|
||||
placeholder={t("common.order_by.start_date")}
|
||||
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
|
||||
optionsClassName="z-10"
|
||||
disabled={isReadOnly}
|
||||
renderByDefault={isMobile}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
{/* target/due date */}
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
|
||||
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
|
||||
<DateDropdown
|
||||
value={issue?.target_date ?? null}
|
||||
onChange={handleTargetDate}
|
||||
minDate={minDate}
|
||||
placeholder={t("common.order_by.due_date")}
|
||||
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
<DateRangeDropdown
|
||||
value={{
|
||||
from: getDate(issue.start_date) || undefined,
|
||||
to: getDate(issue.target_date) || undefined,
|
||||
}}
|
||||
onSelect={(range) => {
|
||||
handleStartDate(range?.from ?? null);
|
||||
handleTargetDate(range?.to ?? null);
|
||||
}}
|
||||
hideIcon={{
|
||||
from: false,
|
||||
}}
|
||||
isClearable
|
||||
mergeDates
|
||||
buttonVariant={issue.start_date || issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""}
|
||||
clearIconClassName="!text-custom-text-100"
|
||||
optionsClassName="z-10"
|
||||
disabled={isReadOnly}
|
||||
renderByDefault={isMobile}
|
||||
showTooltip
|
||||
renderPlaceholder={false}
|
||||
customTooltipHeading="Date Range"
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
@@ -158,7 +158,7 @@ export const LabelDropdown = (props: ILabelDropdownProps) => {
|
||||
setQuery("");
|
||||
}
|
||||
|
||||
if (query !== "" && e.key === "Enter" && canCreateLabel) {
|
||||
if (query !== "" && e.key === "Enter" && !e.nativeEvent.isComposing && canCreateLabel) {
|
||||
e.preventDefault();
|
||||
await handleAddLabel(query);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
EUserPermissionsLevel,
|
||||
} from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { TIssue, IWorkItemPeekOverview } from "@plane/types";
|
||||
// plane ui
|
||||
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||
// components
|
||||
@@ -24,14 +24,8 @@ import { IssueView, TIssueOperations } from "@/components/issues";
|
||||
import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
|
||||
interface IIssuePeekOverview {
|
||||
embedIssue?: boolean;
|
||||
embedRemoveCurrentNotification?: () => void;
|
||||
is_draft?: boolean;
|
||||
storeType?: EIssuesStoreType;
|
||||
}
|
||||
|
||||
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) => {
|
||||
const {
|
||||
embedIssue = false,
|
||||
embedRemoveCurrentNotification,
|
||||
|
||||
@@ -158,6 +158,7 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
|
||||
target_date: val?.to ? renderFormattedPayloadDate(val.to) : null,
|
||||
});
|
||||
}}
|
||||
mergeDates
|
||||
placeholder={{
|
||||
from: t("start_date"),
|
||||
to: t("end_date"),
|
||||
|
||||
@@ -1,116 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useCallback } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { NOTIFICATION_TABS, TNotificationTab } from "@plane/constants";
|
||||
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui";
|
||||
import { CountChip } from "@/components/common";
|
||||
import {
|
||||
NotificationsLoader,
|
||||
NotificationEmptyState,
|
||||
NotificationSidebarHeader,
|
||||
AppliedFilters,
|
||||
} from "@/components/workspace-notifications";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getNumberCount } from "@/helpers/string.helper";
|
||||
import { cn } from "@plane/utils";
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
import { InboxContentRoot } from "@/components/inbox";
|
||||
// hooks
|
||||
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { NotificationCardListRoot } from "@/plane-web/components/workspace-notifications";
|
||||
export const NotificationsSidebarRoot: FC = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
import { useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
import { useNotificationPreview } from "@/plane-web/hooks/use-notification-preview";
|
||||
|
||||
type NotificationsRootProps = {
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
export const NotificationsRoot = observer(({ workspaceSlug }: NotificationsRootProps) => {
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const {
|
||||
currentSelectedNotificationId,
|
||||
unreadNotificationsCount,
|
||||
loader,
|
||||
setCurrentSelectedNotificationId,
|
||||
notificationLiteByNotificationId,
|
||||
notificationIdsByWorkspaceId,
|
||||
currentNotificationTab,
|
||||
setCurrentNotificationTab,
|
||||
getNotifications,
|
||||
} = useWorkspaceNotifications();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { fetchUserProjectInfo } = useUserPermissions();
|
||||
const { isWorkItem, PeekOverviewComponent, setPeekWorkItem } = useNotificationPreview();
|
||||
// derived values
|
||||
const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined;
|
||||
const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined;
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
|
||||
notificationLiteByNotificationId(currentSelectedNotificationId);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" });
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tabValue: TNotificationTab) => {
|
||||
if (currentNotificationTab !== tabValue) {
|
||||
setCurrentNotificationTab(tabValue);
|
||||
}
|
||||
},
|
||||
[currentNotificationTab, setCurrentNotificationTab]
|
||||
// fetching workspace work item properties
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
|
||||
// fetch workspace notifications
|
||||
const notificationMutation =
|
||||
currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id)
|
||||
? ENotificationLoader.MUTATION_LOADER
|
||||
: ENotificationLoader.INIT_LOADER;
|
||||
const notificationLoader =
|
||||
currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id)
|
||||
? ENotificationQueryParamType.CURRENT
|
||||
: ENotificationQueryParamType.INIT;
|
||||
useSWR(
|
||||
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION_${currentWorkspace?.slug}` : null,
|
||||
currentWorkspace?.slug
|
||||
? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
|
||||
: null
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !workspace) return <></>;
|
||||
// fetching user project member info
|
||||
const { isLoading: projectMemberInfoLoader } = useSWR(
|
||||
workspace_slug && project_id && is_inbox_issue
|
||||
? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}`
|
||||
: null,
|
||||
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
|
||||
);
|
||||
|
||||
const embedRemoveCurrentNotification = useCallback(
|
||||
() => setCurrentSelectedNotificationId(undefined),
|
||||
[setCurrentSelectedNotificationId]
|
||||
);
|
||||
|
||||
// clearing up the selected notifications when unmounting the page
|
||||
useEffect(
|
||||
() => () => {
|
||||
setPeekWorkItem(undefined);
|
||||
},
|
||||
[setCurrentSelectedNotificationId, setPeekWorkItem]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative border-0 md:border-r border-custom-border-200 z-[10] flex-shrink-0 bg-custom-background-100 h-full transition-all overflow-hidden",
|
||||
currentSelectedNotificationId ? "w-0 md:w-2/6" : "w-full md:w-2/6"
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full h-full overflow-hidden flex flex-col">
|
||||
<Row className="h-[3.75rem] border-b border-custom-border-200 flex">
|
||||
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
|
||||
</Row>
|
||||
|
||||
<Header variant={EHeaderVariant.SECONDARY} className="justify-start">
|
||||
{NOTIFICATION_TABS.map((tab) => (
|
||||
<div
|
||||
key={tab.value}
|
||||
className="h-full px-3 relative cursor-pointer"
|
||||
onClick={() => handleTabClick(tab.value)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
`relative h-full flex justify-center items-center gap-1 text-sm transition-all`,
|
||||
currentNotificationTab === tab.value
|
||||
? "text-custom-primary-100"
|
||||
: "text-custom-text-100 hover:text-custom-text-200"
|
||||
)}
|
||||
>
|
||||
<div className="font-medium">{t(tab.i18n_label)}</div>
|
||||
{tab.count(unreadNotificationsCount) > 0 && (
|
||||
<CountChip count={getNumberCount(tab.count(unreadNotificationsCount))} />
|
||||
)}
|
||||
</div>
|
||||
{currentNotificationTab === tab.value && (
|
||||
<div className="border absolute bottom-0 right-0 left-0 rounded-t-md border-custom-primary-100" />
|
||||
<div className={cn("w-full h-full overflow-hidden ", isWorkItem && "overflow-y-auto")}>
|
||||
{!currentSelectedNotificationId ? (
|
||||
<div className="w-full h-screen flex justify-center items-center">
|
||||
<SimpleEmptyState title={t("notification.empty_state.detail.title")} assetPath={resolvedPath} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
|
||||
<>
|
||||
{projectMemberInfoLoader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<InboxContentRoot
|
||||
setIsMobileSidebar={() => {}}
|
||||
isMobileSidebar={false}
|
||||
workspaceSlug={workspace_slug}
|
||||
projectId={project_id}
|
||||
inboxIssueId={issue_id}
|
||||
isNotificationEmbed
|
||||
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Header>
|
||||
|
||||
{/* applied filters */}
|
||||
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
||||
|
||||
{/* rendering notifications */}
|
||||
{loader === "init-loader" ? (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<NotificationsLoader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{notificationIds && notificationIds.length > 0 ? (
|
||||
<ContentWrapper variant={ERowVariant.HUGGING}>
|
||||
<NotificationCardListRoot workspaceSlug={workspaceSlug.toString()} workspaceId={workspace?.id} />
|
||||
</ContentWrapper>
|
||||
) : (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<NotificationEmptyState />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<PeekOverviewComponent embedIssue embedRemoveCurrentNotification={embedRemoveCurrentNotification} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
|
||||
|
||||
if (!workspaceSlug) return <></>;
|
||||
return (
|
||||
<Header className="my-auto">
|
||||
<Header className="my-auto bg-custom-background-100">
|
||||
<Header.LeftItem>
|
||||
<div className="block bg-custom-sidebar-background-100 md:hidden">
|
||||
<SidebarHamburgerToggle />
|
||||
|
||||
@@ -6,3 +6,5 @@ export * from "./header";
|
||||
export * from "./filters";
|
||||
|
||||
export * from "./notification-card";
|
||||
|
||||
export * from "./root";
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
import { FC, useCallback } from "react";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { NOTIFICATION_TABS, TNotificationTab } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui";
|
||||
import { CountChip } from "@/components/common";
|
||||
import {
|
||||
NotificationsLoader,
|
||||
NotificationEmptyState,
|
||||
NotificationSidebarHeader,
|
||||
AppliedFilters,
|
||||
} from "@/components/workspace-notifications";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getNumberCount } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
|
||||
import { NotificationListRoot } from "@/plane-web/components/workspace-notifications/list-root";
|
||||
|
||||
export const NotificationsSidebarRoot: FC = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
// hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const {
|
||||
currentSelectedNotificationId,
|
||||
unreadNotificationsCount,
|
||||
loader,
|
||||
notificationIdsByWorkspaceId,
|
||||
currentNotificationTab,
|
||||
setCurrentNotificationTab,
|
||||
} = useWorkspaceNotifications();
|
||||
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined;
|
||||
const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined;
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tabValue: TNotificationTab) => {
|
||||
if (currentNotificationTab !== tabValue) {
|
||||
setCurrentNotificationTab(tabValue);
|
||||
}
|
||||
},
|
||||
[currentNotificationTab, setCurrentNotificationTab]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !workspace) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative border-0 md:border-r border-custom-border-200 z-[10] flex-shrink-0 bg-custom-background-100 h-full transition-all max-md:overflow-hidden",
|
||||
currentSelectedNotificationId ? "w-0 md:w-2/6" : "w-full md:w-2/6"
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full h-full flex flex-col">
|
||||
<Row className="h-[3.75rem] border-b border-custom-border-200 flex">
|
||||
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
|
||||
</Row>
|
||||
|
||||
<Header variant={EHeaderVariant.SECONDARY} className="justify-start">
|
||||
{NOTIFICATION_TABS.map((tab) => (
|
||||
<div
|
||||
key={tab.value}
|
||||
className="h-full px-3 relative cursor-pointer"
|
||||
onClick={() => handleTabClick(tab.value)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
`relative h-full flex justify-center items-center gap-1 text-sm transition-all`,
|
||||
currentNotificationTab === tab.value
|
||||
? "text-custom-primary-100"
|
||||
: "text-custom-text-100 hover:text-custom-text-200"
|
||||
)}
|
||||
>
|
||||
<div className="font-medium">{t(tab.i18n_label)}</div>
|
||||
{tab.count(unreadNotificationsCount) > 0 && (
|
||||
<CountChip count={getNumberCount(tab.count(unreadNotificationsCount))} />
|
||||
)}
|
||||
</div>
|
||||
{currentNotificationTab === tab.value && (
|
||||
<div className="border absolute bottom-0 right-0 left-0 rounded-t-md border-custom-primary-100" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Header>
|
||||
|
||||
{/* applied filters */}
|
||||
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
||||
|
||||
{/* rendering notifications */}
|
||||
{loader === "init-loader" ? (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<NotificationsLoader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{notificationIds && notificationIds.length > 0 ? (
|
||||
<ContentWrapper variant={ERowVariant.HUGGING}>
|
||||
<NotificationListRoot workspaceSlug={workspaceSlug.toString()} workspaceId={workspace?.id} />
|
||||
</ContentWrapper>
|
||||
) : (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<NotificationEmptyState />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -21,7 +21,7 @@ export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKey
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
|
||||
stopEventPropagation(event);
|
||||
onEnterKeyDown();
|
||||
} else if (event.key === "Escape") {
|
||||
|
||||
@@ -204,7 +204,7 @@ export class IssueDetail implements IIssueDetail {
|
||||
this.commentReaction = new IssueCommentReactionStore(this);
|
||||
this.subIssues = new IssueSubIssuesStore(this, serviceType);
|
||||
this.link = new IssueLinkStore(this, serviceType);
|
||||
this.subscription = new IssueSubscriptionStore(this);
|
||||
this.subscription = new IssueSubscriptionStore(this, serviceType);
|
||||
this.relation = new IssueRelationStore(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import set from "lodash/set";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// services
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { IssueService } from "@/services/issue/issue.service";
|
||||
// types
|
||||
import { IIssueDetail } from "./root.store";
|
||||
|
||||
export interface IIssueSubscriptionStoreActions {
|
||||
addSubscription: (issueId: string, isSubscribed: boolean | undefined | null) => void;
|
||||
fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<boolean>;
|
||||
@@ -27,7 +27,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
|
||||
// services
|
||||
issueService;
|
||||
|
||||
constructor(rootStore: IIssueDetail) {
|
||||
constructor(rootStore: IIssueDetail, serviceType: EIssueServiceType) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
subscriptionMap: observable,
|
||||
@@ -40,7 +40,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
|
||||
// root store
|
||||
this.rootIssueDetail = rootStore;
|
||||
// services
|
||||
this.issueService = new IssueService();
|
||||
this.issueService = new IssueService(serviceType);
|
||||
}
|
||||
|
||||
// helper methods
|
||||
|
||||
@@ -177,7 +177,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
|
||||
original_role: projectMember.original_role,
|
||||
member: {
|
||||
...userDetails,
|
||||
joining_date: projectMember.created_at,
|
||||
joining_date: projectMember.created_at ?? undefined,
|
||||
},
|
||||
created_at: projectMember.created_at,
|
||||
};
|
||||
|
||||
@@ -4240,15 +4240,6 @@ bind-event-listener@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz#c90f9a7fcb65cac21045f810c20ef7e647a74921"
|
||||
integrity sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==
|
||||
|
||||
bl@^4.0.3:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
|
||||
dependencies:
|
||||
buffer "^5.5.0"
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
bluebird@^3.7.2:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
@@ -4333,14 +4324,6 @@ buffer-from@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
buffer@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
buffer@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
|
||||
@@ -4506,11 +4489,6 @@ chokidar@3.6.0, chokidar@^3.3.0, chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chownr@^1.1.1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||
|
||||
chromatic@^11.4.0:
|
||||
version "11.25.2"
|
||||
resolved "https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz#cb93dc1332d8f6b70d97a3ef126bc6d03429d396"
|
||||
@@ -5442,7 +5420,7 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
@@ -6315,11 +6293,6 @@ fresh@0.5.2:
|
||||
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs-extra@^10.0.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
|
||||
@@ -6780,7 +6753,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
|
||||
resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
||||
|
||||
ieee754@^1.1.13, ieee754@^1.2.1:
|
||||
ieee754@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
@@ -6833,7 +6806,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@@ -8019,7 +7992,7 @@ minipass@^4.2.4:
|
||||
resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||
mkdirp-classic@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
|
||||
@@ -9474,7 +9447,7 @@ read-cache@^1.0.0:
|
||||
dependencies:
|
||||
pify "^2.3.0"
|
||||
|
||||
readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.2:
|
||||
readable-stream@^3.4.0, readable-stream@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
|
||||
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
|
||||
@@ -10527,20 +10500,10 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
|
||||
resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
||||
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
|
||||
|
||||
tar-fs@^2.0.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz#425f154f3404cb16cb8ff6e671d45ab2ed9596c5"
|
||||
integrity sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==
|
||||
dependencies:
|
||||
chownr "^1.1.1"
|
||||
mkdirp-classic "^0.5.2"
|
||||
pump "^3.0.0"
|
||||
tar-stream "^2.1.4"
|
||||
|
||||
tar-fs@^3.0.4:
|
||||
version "3.0.8"
|
||||
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz#8f62012537d5ff89252d01e48690dc4ebed33ab7"
|
||||
integrity sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==
|
||||
tar-fs@3.0.9, tar-fs@^2.0.0, tar-fs@^3.0.4:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz#d570793c6370d7078926c41fa422891566a0b617"
|
||||
integrity sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
tar-stream "^3.1.5"
|
||||
@@ -10548,17 +10511,6 @@ tar-fs@^3.0.4:
|
||||
bare-fs "^4.0.1"
|
||||
bare-path "^3.0.0"
|
||||
|
||||
tar-stream@^2.1.4:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
||||
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
|
||||
dependencies:
|
||||
bl "^4.0.3"
|
||||
end-of-stream "^1.4.1"
|
||||
fs-constants "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
tar-stream@^3.1.5:
|
||||
version "3.1.7"
|
||||
resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b"
|
||||
@@ -10866,47 +10818,47 @@ tunnel-agent@^0.6.0:
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
turbo-darwin-64@2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.5.3.tgz#e1f19e816f76e0d636e31e66f8238c43bf870f45"
|
||||
integrity sha512-YSItEVBUIvAGPUDpAB9etEmSqZI3T6BHrkBkeSErvICXn3dfqXUfeLx35LfptLDEbrzFUdwYFNmt8QXOwe9yaw==
|
||||
turbo-darwin-64@2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.5.4.tgz#f03b3f071365c626d05e84d0d4b9db96348f2951"
|
||||
integrity sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ==
|
||||
|
||||
turbo-darwin-arm64@2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.3.tgz#f80074fd786f703bcb0415e13df225ba781950fc"
|
||||
integrity sha512-5PefrwHd42UiZX7YA9m1LPW6x9YJBDErXmsegCkVp+GjmWrADfEOxpFrGQNonH3ZMj77WZB2PVE5Aw3gA+IOhg==
|
||||
turbo-darwin-arm64@2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.4.tgz#614becf281da75af5a01094373f380ac2b48190a"
|
||||
integrity sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A==
|
||||
|
||||
turbo-linux-64@2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.5.3.tgz#93bfe009a24a76295c8164896845b5098c293293"
|
||||
integrity sha512-M9xigFgawn5ofTmRzvjjLj3Lqc05O8VHKuOlWNUlnHPUltFquyEeSkpQNkE/vpPdOR14AzxqHbhhxtfS4qvb1w==
|
||||
turbo-linux-64@2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.5.4.tgz#fc7425f35feb19a8373e7926eccdd4a3b2fb77a4"
|
||||
integrity sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA==
|
||||
|
||||
turbo-linux-arm64@2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.5.3.tgz#bf4664561094711aa289d92b9443de2aefca5d6e"
|
||||
integrity sha512-auJRbYZ8SGJVqvzTikpg1bsRAsiI9Tk0/SDkA5Xgg0GdiHDH/BOzv1ZjDE2mjmlrO/obr19Dw+39OlMhwLffrw==
|
||||
turbo-linux-arm64@2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.5.4.tgz#ab5418a88089e4ec20b2c28c3d3c6b8a99b07a81"
|
||||
integrity sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg==
|
||||
|
||||
turbo-windows-64@2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.5.3.tgz#acbc2db093c7a74f0e692b899e649284285a2e7b"
|
||||
integrity sha512-arLQYohuHtIEKkmQSCU9vtrKUg+/1TTstWB9VYRSsz+khvg81eX6LYHtXJfH/dK7Ho6ck+JaEh5G+QrE1jEmCQ==
|
||||
turbo-windows-64@2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.4.tgz#fc78585d3950e5cd64bed8d6648f078e7f32f9a5"
|
||||
integrity sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA==
|
||||
|
||||
turbo-windows-arm64@2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.5.3.tgz#78e8cfdb49b69fbadf03031532f1524be3661729"
|
||||
integrity sha512-3JPn66HAynJ0gtr6H+hjY4VHpu1RPKcEwGATvGUTmLmYSYBQieVlnGDRMMoYN066YfyPqnNGCfhYbXfH92Cm0g==
|
||||
turbo-windows-arm64@2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.5.4.tgz#5134537cc9fa27f4647f2f899b7ba2dacfc6ad22"
|
||||
integrity sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A==
|
||||
|
||||
turbo@^2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.5.3.tgz#657dcae430552d9bb237e9e1d91f711c465c9f28"
|
||||
integrity sha512-iHuaNcq5GZZnr3XDZNuu2LSyCzAOPwDuo5Qt+q64DfsTP1i3T2bKfxJhni2ZQxsvAoxRbuUK5QetJki4qc5aYA==
|
||||
turbo@^2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.npmjs.org/turbo/-/turbo-2.5.4.tgz#e46213a4560b94e56c014e0fd56d06605de16753"
|
||||
integrity sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==
|
||||
optionalDependencies:
|
||||
turbo-darwin-64 "2.5.3"
|
||||
turbo-darwin-arm64 "2.5.3"
|
||||
turbo-linux-64 "2.5.3"
|
||||
turbo-linux-arm64 "2.5.3"
|
||||
turbo-windows-64 "2.5.3"
|
||||
turbo-windows-arm64 "2.5.3"
|
||||
turbo-darwin-64 "2.5.4"
|
||||
turbo-darwin-arm64 "2.5.4"
|
||||
turbo-linux-64 "2.5.4"
|
||||
turbo-linux-arm64 "2.5.4"
|
||||
turbo-windows-64 "2.5.4"
|
||||
turbo-windows-arm64 "2.5.4"
|
||||
|
||||
tween-functions@^1.2.0:
|
||||
version "1.2.0"
|
||||
@@ -11190,11 +11142,6 @@ use-callback-ref@^1.3.3:
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-debounce@^9.0.4:
|
||||
version "9.0.4"
|
||||
resolved "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85"
|
||||
integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==
|
||||
|
||||
use-font-face-observer@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/use-font-face-observer/-/use-font-face-observer-1.2.2.tgz#ed230d907589c6b17e8c8b896c9f5913968ac5ed"
|
||||
|
||||
Reference in New Issue
Block a user