Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 039a8233a1 | |||
| c06e267206 | |||
| 36bf90ca2b | |||
| 296f579997 | |||
| 2c890af224 | |||
| 8d5a549af7 | |||
| c54294388d | |||
| 678e06eb62 | |||
| 4a7e67dc3d | |||
| 526b7061ef | |||
| 12a499a95a | |||
| 171b2c5022 |
@@ -44,7 +44,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
|
||||
loading: "Saving Configuration...",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `GitHub authentication is now ${value ? "active" : "disabled"}.`,
|
||||
message: () => `Github authentication is now ${value ? "active" : "disabled"}.`,
|
||||
},
|
||||
error: {
|
||||
title: "Error",
|
||||
@@ -67,8 +67,8 @@ const InstanceGithubAuthenticationPage = observer(() => {
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<AuthenticationMethodCard
|
||||
name="GitHub"
|
||||
description="Allow members to login or sign up to plane with their GitHub accounts."
|
||||
name="Github"
|
||||
description="Allow members to login or sign up to plane with their Github accounts."
|
||||
icon={
|
||||
<Image
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const InstanceHeader: FC = observer(() => {
|
||||
case "google":
|
||||
return "Google";
|
||||
case "github":
|
||||
return "GitHub";
|
||||
return "Github";
|
||||
case "gitlab":
|
||||
return "GitLab";
|
||||
case "workspace":
|
||||
|
||||
@@ -4,7 +4,7 @@ from rest_framework import serializers
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import Cycle, CycleIssue
|
||||
from plane.utils.timezone_converter import convert_to_utc
|
||||
|
||||
|
||||
class CycleSerializer(BaseSerializer):
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
@@ -24,18 +24,6 @@ class CycleSerializer(BaseSerializer):
|
||||
and data.get("start_date", None) > data.get("end_date", None)
|
||||
):
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("end_date", None) is not None
|
||||
):
|
||||
project_id = self.initial_data.get("project_id") or self.instance.project_id
|
||||
data["start_date"] = convert_to_utc(
|
||||
str(data.get("start_date").date()), project_id, is_start_date=True
|
||||
)
|
||||
data["end_date"] = convert_to_utc(
|
||||
str(data.get("end_date", None).date()), project_id
|
||||
)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -5,7 +5,6 @@ from rest_framework import serializers
|
||||
from .base import BaseSerializer
|
||||
from .issue import IssueStateSerializer
|
||||
from plane.db.models import Cycle, CycleIssue, CycleUserProperties
|
||||
from plane.utils.timezone_converter import convert_to_utc
|
||||
|
||||
|
||||
class CycleWriteSerializer(BaseSerializer):
|
||||
@@ -16,17 +15,6 @@ class CycleWriteSerializer(BaseSerializer):
|
||||
and data.get("start_date", None) > data.get("end_date", None)
|
||||
):
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("end_date", None) is not None
|
||||
):
|
||||
project_id = self.initial_data.get("project_id") or self.instance.project_id
|
||||
data["start_date"] = convert_to_utc(
|
||||
str(data.get("start_date").date()), project_id, is_start_date=True
|
||||
)
|
||||
data["end_date"] = convert_to_utc(
|
||||
str(data.get("end_date", None).date()), project_id
|
||||
)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Python imports
|
||||
import json
|
||||
import pytz
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
@@ -54,11 +52,6 @@ from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
# Module imports
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.utils.timezone_converter import (
|
||||
convert_utc_to_project_timezone,
|
||||
convert_to_utc,
|
||||
user_timezone_converter,
|
||||
)
|
||||
|
||||
|
||||
class CycleViewSet(BaseViewSet):
|
||||
@@ -74,19 +67,6 @@ class CycleViewSet(BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
|
||||
project = Project.objects.get(id=self.kwargs.get("project_id"))
|
||||
|
||||
# Fetch project for the specific record or pass project_id dynamically
|
||||
project_timezone = project.timezone
|
||||
|
||||
# Convert the current time (timezone.now()) to the project's timezone
|
||||
local_tz = pytz.timezone(project_timezone)
|
||||
current_time_in_project_tz = timezone.now().astimezone(local_tz)
|
||||
|
||||
# Convert project local time back to UTC for comparison (start_date is stored in UTC)
|
||||
current_time_in_utc = current_time_in_project_tz.astimezone(pytz.utc)
|
||||
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
@@ -139,15 +119,12 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=current_time_in_utc)
|
||||
& Q(end_date__gte=current_time_in_utc),
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(
|
||||
start_date__gt=current_time_in_utc,
|
||||
then=Value("UPCOMING"),
|
||||
),
|
||||
When(end_date__lt=current_time_in_utc, then=Value("COMPLETED")),
|
||||
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
|
||||
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||
When(
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
then=Value("DRAFT"),
|
||||
@@ -183,22 +160,10 @@ class CycleViewSet(BaseViewSet):
|
||||
# Update the order by
|
||||
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||
|
||||
project = Project.objects.get(id=self.kwargs.get("project_id"))
|
||||
|
||||
# Fetch project for the specific record or pass project_id dynamically
|
||||
project_timezone = project.timezone
|
||||
|
||||
# Convert the current time (timezone.now()) to the project's timezone
|
||||
local_tz = pytz.timezone(project_timezone)
|
||||
current_time_in_project_tz = timezone.now().astimezone(local_tz)
|
||||
|
||||
# Convert project local time back to UTC for comparison (start_date is stored in UTC)
|
||||
current_time_in_utc = current_time_in_project_tz.astimezone(pytz.utc)
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
queryset = queryset.filter(
|
||||
start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc
|
||||
start_date__lte=timezone.now(), end_date__gte=timezone.now()
|
||||
)
|
||||
|
||||
data = queryset.values(
|
||||
@@ -226,8 +191,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"version",
|
||||
"created_by",
|
||||
)
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
data = user_timezone_converter(data, datetime_fields, project_timezone)
|
||||
|
||||
if data:
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
@@ -258,8 +221,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"version",
|
||||
"created_by",
|
||||
)
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
data = user_timezone_converter(data, datetime_fields, request.user.user_timezone)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@@ -456,8 +417,6 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
queryset = queryset.first()
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
data = user_timezone_converter(data, datetime_fields, request.user.user_timezone)
|
||||
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
@@ -533,9 +492,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
start_date = convert_to_utc(str(start_date), project_id, is_start_date=True)
|
||||
end_date = convert_to_utc(str(end_date), project_id)
|
||||
|
||||
# Check if any cycle intersects in the given interval
|
||||
cycles = Cycle.objects.filter(
|
||||
Q(workspace__slug=slug)
|
||||
|
||||
@@ -54,11 +54,10 @@ from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.global_paginator import paginate
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
|
||||
|
||||
class IssueListEndpoint(BaseAPIView):
|
||||
@@ -429,13 +428,6 @@ class IssueViewSet(BaseViewSet):
|
||||
slug=slug,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
updated_issue=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
issue_id=str(serializer.data["id"]),
|
||||
user_id=request.user.id,
|
||||
is_creating=True,
|
||||
)
|
||||
return Response(issue, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -657,12 +649,6 @@ class IssueViewSet(BaseViewSet):
|
||||
slug=slug,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
updated_issue=current_instance,
|
||||
issue_id=str(serializer.data.get("id", None)),
|
||||
user_id=request.user.id,
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from plane.app.serializers import IssueSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Issue, IssueLink, FileAsset, CycleIssue
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.serializers import ModuleDetailSerializer
|
||||
from plane.db.models import Issue, Module, ModuleLink, UserFavorite, Project
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
|
||||
# Module imports
|
||||
|
||||
@@ -56,7 +56,7 @@ from plane.db.models import (
|
||||
Project,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
|
||||
@@ -10,7 +10,7 @@ from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import Cycle
|
||||
from plane.app.permissions import WorkspaceViewerPermission
|
||||
from plane.app.serializers.cycle import CycleSerializer
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
|
||||
|
||||
class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceViewerPermission]
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# Python imports
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue, IssueDescriptionVersion, ProjectMember
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
def get_owner_id(issue: Issue) -> Optional[int]:
|
||||
"""Get the owner ID of the issue"""
|
||||
|
||||
if issue.updated_by_id:
|
||||
return issue.updated_by_id
|
||||
|
||||
if issue.created_by_id:
|
||||
return issue.created_by_id
|
||||
|
||||
# Find project admin as fallback
|
||||
project_member = ProjectMember.objects.filter(
|
||||
project_id=issue.project_id,
|
||||
role=20, # Admin role
|
||||
).first()
|
||||
|
||||
return project_member.member_id if project_member else None
|
||||
|
||||
|
||||
@shared_task
|
||||
def sync_issue_description_version(batch_size=5000, offset=0, countdown=300):
|
||||
"""Task to create IssueDescriptionVersion records for existing Issues in batches"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
base_query = Issue.objects
|
||||
total_issues_count = base_query.count()
|
||||
|
||||
if total_issues_count == 0:
|
||||
return
|
||||
|
||||
# Calculate batch range
|
||||
end_offset = min(offset + batch_size, total_issues_count)
|
||||
|
||||
# Fetch issues with related data
|
||||
issues_batch = (
|
||||
base_query.order_by("created_at")
|
||||
.select_related("workspace", "project")
|
||||
.only(
|
||||
"id",
|
||||
"workspace_id",
|
||||
"project_id",
|
||||
"created_by_id",
|
||||
"updated_by_id",
|
||||
"description_binary",
|
||||
"description_html",
|
||||
"description_stripped",
|
||||
"description",
|
||||
)[offset:end_offset]
|
||||
)
|
||||
|
||||
if not issues_batch:
|
||||
return
|
||||
|
||||
version_objects = []
|
||||
for issue in issues_batch:
|
||||
# Validate required fields
|
||||
if not issue.workspace_id or not issue.project_id:
|
||||
logging.warning(
|
||||
f"Skipping {issue.id} - missing workspace_id or project_id"
|
||||
)
|
||||
continue
|
||||
|
||||
# Determine owned_by_id
|
||||
owned_by_id = get_owner_id(issue)
|
||||
if owned_by_id is None:
|
||||
logging.warning(f"Skipping issue {issue.id} - missing owned_by")
|
||||
continue
|
||||
|
||||
# Create version object
|
||||
version_objects.append(
|
||||
IssueDescriptionVersion(
|
||||
workspace_id=issue.workspace_id,
|
||||
project_id=issue.project_id,
|
||||
created_by_id=issue.created_by_id,
|
||||
updated_by_id=issue.updated_by_id,
|
||||
owned_by_id=owned_by_id,
|
||||
last_saved_at=timezone.now(),
|
||||
issue_id=issue.id,
|
||||
description_binary=issue.description_binary,
|
||||
description_html=issue.description_html,
|
||||
description_stripped=issue.description_stripped,
|
||||
description_json=issue.description,
|
||||
)
|
||||
)
|
||||
|
||||
# Bulk create version objects
|
||||
if version_objects:
|
||||
IssueDescriptionVersion.objects.bulk_create(version_objects)
|
||||
|
||||
# Schedule next batch if needed
|
||||
if end_offset < total_issues_count:
|
||||
sync_issue_description_version.apply_async(
|
||||
kwargs={
|
||||
"batch_size": batch_size,
|
||||
"offset": end_offset,
|
||||
"countdown": countdown,
|
||||
},
|
||||
countdown=countdown,
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@shared_task
|
||||
def schedule_issue_description_version(batch_size=5000, countdown=300):
|
||||
sync_issue_description_version.delay(
|
||||
batch_size=int(batch_size), countdown=countdown
|
||||
)
|
||||
@@ -1,84 +0,0 @@
|
||||
from celery import shared_task
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from typing import Optional, Dict
|
||||
import json
|
||||
|
||||
from plane.db.models import Issue, IssueDescriptionVersion
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
def should_update_existing_version(
|
||||
version: IssueDescriptionVersion, user_id: str, max_time_difference: int = 600
|
||||
) -> bool:
|
||||
if not version:
|
||||
return
|
||||
|
||||
time_difference = (timezone.now() - version.last_saved_at).total_seconds()
|
||||
return (
|
||||
str(version.owned_by_id) == str(user_id)
|
||||
and time_difference <= max_time_difference
|
||||
)
|
||||
|
||||
|
||||
def update_existing_version(version: IssueDescriptionVersion, issue) -> None:
|
||||
version.description_json = issue.description
|
||||
version.description_html = issue.description_html
|
||||
version.description_binary = issue.description_binary
|
||||
version.description_stripped = issue.description_stripped
|
||||
version.last_saved_at = timezone.now()
|
||||
|
||||
version.save(
|
||||
update_fields=[
|
||||
"description_json",
|
||||
"description_html",
|
||||
"description_binary",
|
||||
"description_stripped",
|
||||
"last_saved_at",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def issue_description_version_task(
|
||||
updated_issue, issue_id, user_id, is_creating=False
|
||||
) -> Optional[bool]:
|
||||
try:
|
||||
# Parse updated issue data
|
||||
current_issue: Dict = json.loads(updated_issue) if updated_issue else {}
|
||||
|
||||
# Get current issue
|
||||
issue = Issue.objects.get(id=issue_id)
|
||||
|
||||
# Check if description has changed
|
||||
if (
|
||||
current_issue.get("description_html") == issue.description_html
|
||||
and not is_creating
|
||||
):
|
||||
return
|
||||
|
||||
with transaction.atomic():
|
||||
# Get latest version
|
||||
latest_version = (
|
||||
IssueDescriptionVersion.objects.filter(issue_id=issue_id)
|
||||
.order_by("-last_saved_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
# Determine whether to update existing or create new version
|
||||
if should_update_existing_version(version=latest_version, user_id=user_id):
|
||||
update_existing_version(latest_version, issue)
|
||||
else:
|
||||
IssueDescriptionVersion.log_issue_description_version(issue, user_id)
|
||||
|
||||
return
|
||||
|
||||
except Issue.DoesNotExist:
|
||||
# Issue no longer exists, skip processing
|
||||
return
|
||||
except json.JSONDecodeError as e:
|
||||
log_exception(f"Invalid JSON for updated_issue: {e}")
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(f"Error processing issue description version: {e}")
|
||||
return
|
||||
@@ -1,254 +0,0 @@
|
||||
# Python imports
|
||||
import json
|
||||
from typing import Optional, List, Dict
|
||||
from uuid import UUID
|
||||
from itertools import groupby
|
||||
import logging
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueVersion,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
ModuleIssue,
|
||||
IssueActivity,
|
||||
IssueAssignee,
|
||||
IssueLabel,
|
||||
)
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def issue_task(updated_issue, issue_id, user_id):
|
||||
try:
|
||||
current_issue = json.loads(updated_issue) if updated_issue else {}
|
||||
issue = Issue.objects.get(id=issue_id)
|
||||
|
||||
updated_current_issue = {}
|
||||
for key, value in current_issue.items():
|
||||
if getattr(issue, key) != value:
|
||||
updated_current_issue[key] = value
|
||||
|
||||
if updated_current_issue:
|
||||
issue_version = (
|
||||
IssueVersion.objects.filter(issue_id=issue_id)
|
||||
.order_by("-last_saved_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
if (
|
||||
issue_version
|
||||
and str(issue_version.owned_by) == str(user_id)
|
||||
and (timezone.now() - issue_version.last_saved_at).total_seconds()
|
||||
<= 600
|
||||
):
|
||||
for key, value in updated_current_issue.items():
|
||||
setattr(issue_version, key, value)
|
||||
issue_version.last_saved_at = timezone.now()
|
||||
issue_version.save(
|
||||
update_fields=list(updated_current_issue.keys()) + ["last_saved_at"]
|
||||
)
|
||||
else:
|
||||
IssueVersion.log_issue_version(issue, user_id)
|
||||
|
||||
return
|
||||
except Issue.DoesNotExist:
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
def get_owner_id(issue: Issue) -> Optional[int]:
|
||||
"""Get the owner ID of the issue"""
|
||||
|
||||
if issue.updated_by_id:
|
||||
return issue.updated_by_id
|
||||
|
||||
if issue.created_by_id:
|
||||
return issue.created_by_id
|
||||
|
||||
# Find project admin as fallback
|
||||
project_member = ProjectMember.objects.filter(
|
||||
project_id=issue.project_id,
|
||||
role=20, # Admin role
|
||||
).first()
|
||||
|
||||
return project_member.member_id if project_member else None
|
||||
|
||||
|
||||
def get_related_data(issue_ids: List[UUID]) -> Dict:
|
||||
"""Get related data for the given issue IDs"""
|
||||
|
||||
cycle_issues = {
|
||||
ci.issue_id: ci.cycle_id
|
||||
for ci in CycleIssue.objects.filter(issue_id__in=issue_ids)
|
||||
}
|
||||
|
||||
# Get assignees with proper grouping
|
||||
assignee_records = list(
|
||||
IssueAssignee.objects.filter(issue_id__in=issue_ids)
|
||||
.values_list("issue_id", "assignee_id")
|
||||
.order_by("issue_id")
|
||||
)
|
||||
assignees = {}
|
||||
for issue_id, group in groupby(assignee_records, key=lambda x: x[0]):
|
||||
assignees[issue_id] = [str(g[1]) for g in group]
|
||||
|
||||
# Get labels with proper grouping
|
||||
label_records = list(
|
||||
IssueLabel.objects.filter(issue_id__in=issue_ids)
|
||||
.values_list("issue_id", "label_id")
|
||||
.order_by("issue_id")
|
||||
)
|
||||
labels = {}
|
||||
for issue_id, group in groupby(label_records, key=lambda x: x[0]):
|
||||
labels[issue_id] = [str(g[1]) for g in group]
|
||||
|
||||
# Get modules with proper grouping
|
||||
module_records = list(
|
||||
ModuleIssue.objects.filter(issue_id__in=issue_ids)
|
||||
.values_list("issue_id", "module_id")
|
||||
.order_by("issue_id")
|
||||
)
|
||||
modules = {}
|
||||
for issue_id, group in groupby(module_records, key=lambda x: x[0]):
|
||||
modules[issue_id] = [str(g[1]) for g in group]
|
||||
|
||||
# Get latest activities
|
||||
latest_activities = {}
|
||||
activities = IssueActivity.objects.filter(issue_id__in=issue_ids).order_by(
|
||||
"issue_id", "-created_at"
|
||||
)
|
||||
for issue_id, activities_group in groupby(activities, key=lambda x: x.issue_id):
|
||||
first_activity = next(activities_group, None)
|
||||
if first_activity:
|
||||
latest_activities[issue_id] = first_activity.id
|
||||
|
||||
return {
|
||||
"cycle_issues": cycle_issues,
|
||||
"assignees": assignees,
|
||||
"labels": labels,
|
||||
"modules": modules,
|
||||
"activities": latest_activities,
|
||||
}
|
||||
|
||||
|
||||
def create_issue_version(issue: Issue, related_data: Dict) -> Optional[IssueVersion]:
|
||||
"""Create IssueVersion object from the given issue and related data"""
|
||||
|
||||
try:
|
||||
if not issue.workspace_id or not issue.project_id:
|
||||
logging.warning(
|
||||
f"Skipping issue {issue.id} - missing workspace_id or project_id"
|
||||
)
|
||||
return None
|
||||
|
||||
owned_by_id = get_owner_id(issue)
|
||||
if owned_by_id is None:
|
||||
logging.warning(f"Skipping issue {issue.id} - missing owned_by")
|
||||
return None
|
||||
|
||||
return IssueVersion(
|
||||
workspace_id=issue.workspace_id,
|
||||
project_id=issue.project_id,
|
||||
created_by_id=issue.created_by_id,
|
||||
updated_by_id=issue.updated_by_id,
|
||||
owned_by_id=owned_by_id,
|
||||
last_saved_at=timezone.now(),
|
||||
activity_id=related_data["activities"].get(issue.id),
|
||||
properties=getattr(issue, "properties", {}),
|
||||
meta=getattr(issue, "meta", {}),
|
||||
issue_id=issue.id,
|
||||
parent=issue.parent_id,
|
||||
state=issue.state_id,
|
||||
estimate_point=issue.estimate_point_id,
|
||||
name=issue.name,
|
||||
priority=issue.priority,
|
||||
start_date=issue.start_date,
|
||||
target_date=issue.target_date,
|
||||
assignees=related_data["assignees"].get(issue.id, []),
|
||||
sequence_id=issue.sequence_id,
|
||||
labels=related_data["labels"].get(issue.id, []),
|
||||
sort_order=issue.sort_order,
|
||||
completed_at=issue.completed_at,
|
||||
archived_at=issue.archived_at,
|
||||
is_draft=issue.is_draft,
|
||||
external_source=issue.external_source,
|
||||
external_id=issue.external_id,
|
||||
type=issue.type_id,
|
||||
cycle=related_data["cycle_issues"].get(issue.id),
|
||||
modules=related_data["modules"].get(issue.id, []),
|
||||
)
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
|
||||
@shared_task
|
||||
def sync_issue_version(batch_size=5000, offset=0, countdown=300):
|
||||
"""Task to create IssueVersion records for existing Issues in batches"""
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
base_query = Issue.objects
|
||||
total_issues_count = base_query.count()
|
||||
|
||||
if total_issues_count == 0:
|
||||
return
|
||||
|
||||
end_offset = min(offset + batch_size, total_issues_count)
|
||||
|
||||
# Get issues batch with optimized queries
|
||||
issues_batch = list(
|
||||
base_query.order_by("created_at")
|
||||
.select_related("workspace", "project")
|
||||
.all()[offset:end_offset]
|
||||
)
|
||||
|
||||
if not issues_batch:
|
||||
return
|
||||
|
||||
# Get all related data in bulk
|
||||
issue_ids = [issue.id for issue in issues_batch]
|
||||
related_data = get_related_data(issue_ids)
|
||||
|
||||
issue_versions = []
|
||||
for issue in issues_batch:
|
||||
version = create_issue_version(issue, related_data)
|
||||
if version:
|
||||
issue_versions.append(version)
|
||||
|
||||
# Bulk create versions
|
||||
if issue_versions:
|
||||
IssueVersion.objects.bulk_create(issue_versions, batch_size=1000)
|
||||
|
||||
# Schedule the next batch if there are more workspaces to process
|
||||
if end_offset < total_issues_count:
|
||||
sync_issue_version.apply_async(
|
||||
kwargs={
|
||||
"batch_size": batch_size,
|
||||
"offset": end_offset,
|
||||
"countdown": countdown,
|
||||
},
|
||||
countdown=countdown,
|
||||
)
|
||||
|
||||
logging.info(f"Processed Issues: {end_offset}")
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@shared_task
|
||||
def schedule_issue_version(batch_size=5000, countdown=300):
|
||||
sync_issue_version.delay(batch_size=int(batch_size), countdown=countdown)
|
||||
@@ -1,23 +0,0 @@
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# Module imports
|
||||
from plane.bgtasks.issue_description_version_sync import (
|
||||
schedule_issue_description_version,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates IssueDescriptionVersion records for existing Issues in batches"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
batch_size = input("Enter the batch size: ")
|
||||
batch_countdown = input("Enter the batch countdown: ")
|
||||
|
||||
schedule_issue_description_version.delay(
|
||||
batch_size=batch_size, countdown=int(batch_countdown)
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Successfully created issue description version task")
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# Module imports
|
||||
from plane.bgtasks.issue_version_sync import schedule_issue_version
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates IssueVersion records for existing Issues in batches"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
batch_size = input("Enter the batch size: ")
|
||||
batch_countdown = input("Enter the batch countdown: ")
|
||||
|
||||
schedule_issue_version.delay(
|
||||
batch_size=batch_size, countdown=int(batch_countdown)
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully created issue version task"))
|
||||
@@ -1,117 +0,0 @@
|
||||
# Generated by Django 4.2.17 on 2024-12-13 10:09
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import plane.db.models.user
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0086_issueversion_alter_teampage_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='issueversion',
|
||||
name='description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='issueversion',
|
||||
name='description_binary',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='issueversion',
|
||||
name='description_html',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='issueversion',
|
||||
name='description_stripped',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueversion',
|
||||
name='activity',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='versions', to='db.issueactivity'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_mobile_onboarded',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='mobile_onboarding_step',
|
||||
field=models.JSONField(default=plane.db.models.user.get_mobile_default_onboarding),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='mobile_timezone_auto_set',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='language',
|
||||
field=models.CharField(default='en', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issueversion',
|
||||
name='owned_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_versions', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Sticky',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.TextField()),
|
||||
('description', models.JSONField(blank=True, default=dict)),
|
||||
('description_html', models.TextField(blank=True, default='<p></p>')),
|
||||
('description_stripped', models.TextField(blank=True, null=True)),
|
||||
('description_binary', models.BinaryField(null=True)),
|
||||
('logo_props', models.JSONField(default=dict)),
|
||||
('color', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('background_color', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stickies', to=settings.AUTH_USER_MODEL)),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stickies', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Sticky',
|
||||
'verbose_name_plural': 'Stickies',
|
||||
'db_table': 'stickies',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueDescriptionVersion',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('description_binary', models.BinaryField(null=True)),
|
||||
('description_html', models.TextField(blank=True, default='<p></p>')),
|
||||
('description_stripped', models.TextField(blank=True, null=True)),
|
||||
('description_json', models.JSONField(blank=True, default=dict)),
|
||||
('last_saved_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='description_versions', to='db.issue')),
|
||||
('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_description_versions', to=settings.AUTH_USER_MODEL)),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Description Version',
|
||||
'verbose_name_plural': 'Issue Description Versions',
|
||||
'db_table': 'issue_description_versions',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -41,8 +41,6 @@ from .issue import (
|
||||
IssueSequence,
|
||||
IssueSubscriber,
|
||||
IssueVote,
|
||||
IssueVersion,
|
||||
IssueDescriptionVersion,
|
||||
)
|
||||
from .module import Module, ModuleIssue, ModuleLink, ModuleMember, ModuleUserProperties
|
||||
from .notification import EmailNotificationLog, Notification, UserNotificationPreference
|
||||
@@ -70,6 +68,15 @@ from .workspace import (
|
||||
WorkspaceUserProperties,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
from .favorite import UserFavorite
|
||||
|
||||
from .issue_type import IssueType
|
||||
@@ -79,5 +86,3 @@ from .recent_visit import UserRecentVisit
|
||||
from .label import Label
|
||||
|
||||
from .device import Device, DeviceSession
|
||||
|
||||
from .sticky import Sticky
|
||||
|
||||
@@ -15,7 +15,6 @@ from django import apps
|
||||
from plane.utils.html_processor import strip_tags
|
||||
from plane.db.mixins import SoftDeletionManager
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from .base import BaseModel
|
||||
from .project import ProjectBaseModel
|
||||
|
||||
|
||||
@@ -661,6 +660,9 @@ class IssueVote(ProjectBaseModel):
|
||||
|
||||
|
||||
class IssueVersion(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="versions"
|
||||
)
|
||||
PRIORITY_CHOICES = (
|
||||
("urgent", "Urgent"),
|
||||
("high", "High"),
|
||||
@@ -668,11 +670,14 @@ class IssueVersion(ProjectBaseModel):
|
||||
("low", "Low"),
|
||||
("none", "None"),
|
||||
)
|
||||
|
||||
parent = models.UUIDField(blank=True, null=True)
|
||||
state = models.UUIDField(blank=True, null=True)
|
||||
estimate_point = models.UUIDField(blank=True, null=True)
|
||||
name = models.CharField(max_length=255, verbose_name="Issue Name")
|
||||
description = models.JSONField(blank=True, default=dict)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
priority = models.CharField(
|
||||
max_length=30,
|
||||
choices=PRIORITY_CHOICES,
|
||||
@@ -681,9 +686,7 @@ class IssueVersion(ProjectBaseModel):
|
||||
)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
target_date = models.DateField(null=True, blank=True)
|
||||
assignees = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID")
|
||||
labels = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
completed_at = models.DateTimeField(null=True)
|
||||
archived_at = models.DateField(null=True)
|
||||
@@ -691,26 +694,14 @@ class IssueVersion(ProjectBaseModel):
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
type = models.UUIDField(blank=True, null=True)
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
owned_by = models.UUIDField()
|
||||
assignees = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
labels = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
cycle = models.UUIDField(null=True, blank=True)
|
||||
modules = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
properties = models.JSONField(default=dict) # issue properties
|
||||
meta = models.JSONField(default=dict) # issue meta
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="versions"
|
||||
)
|
||||
activity = models.ForeignKey(
|
||||
"db.IssueActivity",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="versions",
|
||||
)
|
||||
owned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_versions",
|
||||
)
|
||||
properties = models.JSONField(default=dict)
|
||||
meta = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Version"
|
||||
@@ -730,93 +721,39 @@ class IssueVersion(ProjectBaseModel):
|
||||
|
||||
Module = apps.get_model("db.Module")
|
||||
CycleIssue = apps.get_model("db.CycleIssue")
|
||||
IssueAssignee = apps.get_model("db.IssueAssignee")
|
||||
IssueLabel = apps.get_model("db.IssueLabel")
|
||||
|
||||
cycle_issue = CycleIssue.objects.filter(issue=issue).first()
|
||||
|
||||
cls.objects.create(
|
||||
issue=issue,
|
||||
parent=issue.parent_id,
|
||||
state=issue.state_id,
|
||||
estimate_point=issue.estimate_point_id,
|
||||
parent=issue.parent,
|
||||
state=issue.state,
|
||||
point=issue.point,
|
||||
estimate_point=issue.estimate_point,
|
||||
name=issue.name,
|
||||
description=issue.description,
|
||||
description_html=issue.description_html,
|
||||
description_stripped=issue.description_stripped,
|
||||
description_binary=issue.description_binary,
|
||||
priority=issue.priority,
|
||||
start_date=issue.start_date,
|
||||
target_date=issue.target_date,
|
||||
assignees=list(
|
||||
IssueAssignee.objects.filter(issue=issue).values_list(
|
||||
"assignee_id", flat=True
|
||||
)
|
||||
),
|
||||
sequence_id=issue.sequence_id,
|
||||
labels=list(
|
||||
IssueLabel.objects.filter(issue=issue).values_list(
|
||||
"label_id", flat=True
|
||||
)
|
||||
),
|
||||
sort_order=issue.sort_order,
|
||||
completed_at=issue.completed_at,
|
||||
archived_at=issue.archived_at,
|
||||
is_draft=issue.is_draft,
|
||||
external_source=issue.external_source,
|
||||
external_id=issue.external_id,
|
||||
type=issue.type_id,
|
||||
cycle=cycle_issue.cycle_id if cycle_issue else None,
|
||||
modules=list(
|
||||
Module.objects.filter(issue=issue).values_list("id", flat=True)
|
||||
),
|
||||
properties={},
|
||||
meta={},
|
||||
last_saved_at=timezone.now(),
|
||||
type=issue.type,
|
||||
last_saved_at=issue.last_saved_at,
|
||||
assignees=issue.assignees,
|
||||
labels=issue.labels,
|
||||
cycle=cycle_issue.cycle if cycle_issue else None,
|
||||
modules=Module.objects.filter(issue=issue).values_list("id", flat=True),
|
||||
owned_by=user,
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return False
|
||||
|
||||
|
||||
class IssueDescriptionVersion(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="description_versions"
|
||||
)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
description_json = models.JSONField(default=dict, blank=True)
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
owned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_description_versions",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Description Version"
|
||||
verbose_name_plural = "Issue Description Versions"
|
||||
db_table = "issue_description_versions"
|
||||
|
||||
@classmethod
|
||||
def log_issue_description_version(cls, issue, user):
|
||||
try:
|
||||
"""
|
||||
Log the issue description version
|
||||
"""
|
||||
cls.objects.create(
|
||||
workspace_id=issue.workspace_id,
|
||||
project_id=issue.project_id,
|
||||
created_by_id=issue.created_by_id,
|
||||
updated_by_id=issue.updated_by_id,
|
||||
owned_by_id=user,
|
||||
last_saved_at=timezone.now(),
|
||||
issue_id=issue.id,
|
||||
description_binary=issue.description_binary,
|
||||
description_html=issue.description_html,
|
||||
description_stripped=issue.description_stripped,
|
||||
description_json=issue.description,
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return False
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
# Module imports
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class Sticky(BaseModel):
|
||||
name = models.TextField()
|
||||
|
||||
description = models.JSONField(blank=True, default=dict)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
|
||||
logo_props = models.JSONField(default=dict)
|
||||
color = models.CharField(max_length=255, blank=True, null=True)
|
||||
background_color = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="stickies"
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="stickies"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Sticky"
|
||||
verbose_name_plural = "Stickies"
|
||||
db_table = "stickies"
|
||||
ordering = ("-created_at",)
|
||||
@@ -26,14 +26,6 @@ def get_default_onboarding():
|
||||
}
|
||||
|
||||
|
||||
def get_mobile_default_onboarding():
|
||||
return {
|
||||
"profile_complete": False,
|
||||
"workspace_create": False,
|
||||
"workspace_join": False,
|
||||
}
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
|
||||
@@ -186,12 +178,6 @@ class Profile(TimeAuditModel):
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
company_name = models.CharField(max_length=255, blank=True)
|
||||
# mobile
|
||||
is_mobile_onboarded = models.BooleanField(default=False)
|
||||
mobile_onboarding_step = models.JSONField(default=get_mobile_default_onboarding)
|
||||
mobile_timezone_auto_set = models.BooleanField(default=False)
|
||||
# language
|
||||
language = models.CharField(max_length=255, default="en")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Profile"
|
||||
|
||||
@@ -262,9 +262,6 @@ CELERY_IMPORTS = (
|
||||
"plane.license.bgtasks.tracer",
|
||||
# management tasks
|
||||
"plane.bgtasks.dummy_data_task",
|
||||
# issue version tasks
|
||||
"plane.bgtasks.issue_version_sync",
|
||||
"plane.bgtasks.issue_description_version_sync",
|
||||
)
|
||||
|
||||
# Sentry Settings
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import pytz
|
||||
from plane.db.models import Project
|
||||
from datetime import datetime, time
|
||||
from datetime import timedelta
|
||||
|
||||
def user_timezone_converter(queryset, datetime_fields, user_timezone):
|
||||
# Create a timezone object for the user's timezone
|
||||
user_tz = pytz.timezone(user_timezone)
|
||||
|
||||
# Check if queryset is a dictionary (single item) or a list of dictionaries
|
||||
if isinstance(queryset, dict):
|
||||
queryset_values = [queryset]
|
||||
else:
|
||||
queryset_values = list(queryset)
|
||||
|
||||
# Iterate over the dictionaries in the list
|
||||
for item in queryset_values:
|
||||
# Iterate over the datetime fields
|
||||
for field in datetime_fields:
|
||||
# Convert the datetime field to the user's timezone
|
||||
if field in item and item[field]:
|
||||
item[field] = item[field].astimezone(user_tz)
|
||||
|
||||
# If queryset was a single item, return a single item
|
||||
if isinstance(queryset, dict):
|
||||
return queryset_values[0]
|
||||
else:
|
||||
return queryset_values
|
||||
|
||||
|
||||
def convert_to_utc(date, project_id, is_start_date=False):
|
||||
"""
|
||||
Converts a start date string to the project's local timezone at 12:00 AM
|
||||
and then converts it to UTC for storage.
|
||||
|
||||
Args:
|
||||
date (str): The date string in "YYYY-MM-DD" format.
|
||||
project_id (int): The project's ID to fetch the associated timezone.
|
||||
|
||||
Returns:
|
||||
datetime: The UTC datetime.
|
||||
"""
|
||||
# Retrieve the project's timezone using the project ID
|
||||
project = Project.objects.get(id=project_id)
|
||||
project_timezone = project.timezone
|
||||
if not date or not project_timezone:
|
||||
raise ValueError("Both date and timezone must be provided.")
|
||||
|
||||
# Parse the string into a date object
|
||||
start_date = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
|
||||
# Get the project's timezone
|
||||
local_tz = pytz.timezone(project_timezone)
|
||||
|
||||
# Combine the date with 12:00 AM time
|
||||
local_datetime = datetime.combine(start_date, time.min)
|
||||
|
||||
# Localize the datetime to the project's timezone
|
||||
localized_datetime = local_tz.localize(local_datetime)
|
||||
|
||||
# If it's an start date, add one minute
|
||||
if is_start_date:
|
||||
localized_datetime += timedelta(minutes=1)
|
||||
|
||||
# Convert the localized datetime to UTC
|
||||
utc_datetime = localized_datetime.astimezone(pytz.utc)
|
||||
|
||||
# Return the UTC datetime for storage
|
||||
return utc_datetime
|
||||
|
||||
|
||||
def convert_utc_to_project_timezone(utc_datetime, project_id):
|
||||
"""
|
||||
Converts a UTC datetime (stored in the database) to the project's local timezone.
|
||||
|
||||
Args:
|
||||
utc_datetime (datetime): The UTC datetime to be converted.
|
||||
project_id (int): The project's ID to fetch the associated timezone.
|
||||
|
||||
Returns:
|
||||
datetime: The datetime in the project's local timezone.
|
||||
"""
|
||||
# Retrieve the project's timezone using the project ID
|
||||
project = Project.objects.get(id=project_id)
|
||||
project_timezone = project.timezone
|
||||
if not project_timezone:
|
||||
raise ValueError("Project timezone must be provided.")
|
||||
|
||||
# Get the timezone object for the project's timezone
|
||||
local_tz = pytz.timezone(project_timezone)
|
||||
|
||||
# Convert the UTC datetime to the project's local timezone
|
||||
if utc_datetime.tzinfo is None:
|
||||
# Localize UTC datetime if it's naive (i.e., without timezone info)
|
||||
utc_datetime = pytz.utc.localize(utc_datetime)
|
||||
|
||||
# Convert to the project's local timezone
|
||||
local_datetime = utc_datetime.astimezone(local_tz)
|
||||
|
||||
return local_datetime
|
||||
@@ -0,0 +1,26 @@
|
||||
import pytz
|
||||
|
||||
|
||||
def user_timezone_converter(queryset, datetime_fields, user_timezone):
|
||||
# Create a timezone object for the user's timezone
|
||||
user_tz = pytz.timezone(user_timezone)
|
||||
|
||||
# Check if queryset is a dictionary (single item) or a list of dictionaries
|
||||
if isinstance(queryset, dict):
|
||||
queryset_values = [queryset]
|
||||
else:
|
||||
queryset_values = list(queryset)
|
||||
|
||||
# Iterate over the dictionaries in the list
|
||||
for item in queryset_values:
|
||||
# Iterate over the datetime fields
|
||||
for field in datetime_fields:
|
||||
# Convert the datetime field to the user's timezone
|
||||
if field in item and item[field]:
|
||||
item[field] = item[field].astimezone(user_tz)
|
||||
|
||||
# If queryset was a single item, return a single item
|
||||
if isinstance(queryset, dict):
|
||||
return queryset_values[0]
|
||||
else:
|
||||
return queryset_values
|
||||
@@ -70,7 +70,7 @@
|
||||
"value": ""
|
||||
},
|
||||
"GITHUB_CLIENT_SECRET": {
|
||||
"description": "GitHub Client Secret",
|
||||
"description": "Github Client Secret",
|
||||
"value": ""
|
||||
},
|
||||
"NEXT_PUBLIC_API_BASE_URL": {
|
||||
|
||||
@@ -62,7 +62,7 @@ mkdir plane-selfhost
|
||||
|
||||
cd plane-selfhost
|
||||
|
||||
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh
|
||||
curl -fsSL -o setup.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/install.sh
|
||||
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
@@ -1,63 +1,54 @@
|
||||
x-db-env: &db-env
|
||||
PGHOST: ${PGHOST:-plane-db}
|
||||
PGDATABASE: ${PGDATABASE:-plane}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-plane}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-plane}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
|
||||
PGDATA: ${PGDATA:-/var/lib/postgresql/data}
|
||||
|
||||
x-redis-env: &redis-env
|
||||
REDIS_HOST: ${REDIS_HOST:-plane-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/}
|
||||
|
||||
x-minio-env: &minio-env
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
|
||||
x-aws-s3-env: &aws-s3-env
|
||||
AWS_REGION: ${AWS_REGION:-}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
|
||||
x-proxy-env: &proxy-env
|
||||
NGINX_PORT: ${NGINX_PORT:-80}
|
||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
|
||||
x-mq-env: &mq-env
|
||||
# RabbitMQ Settings
|
||||
RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
|
||||
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
|
||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
|
||||
x-live-env: &live-env
|
||||
API_BASE_URL: ${API_BASE_URL:-http://api:8000}
|
||||
|
||||
x-app-env: &app-env
|
||||
WEB_URL: ${WEB_URL:-http://localhost}
|
||||
DEBUG: ${DEBUG:-0}
|
||||
SENTRY_DSN: ${SENTRY_DSN}
|
||||
SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production}
|
||||
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
|
||||
GUNICORN_WORKERS: 1
|
||||
USE_MINIO: ${USE_MINIO:-1}
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
|
||||
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
ADMIN_BASE_URL: ${ADMIN_BASE_URL}
|
||||
SPACE_BASE_URL: ${SPACE_BASE_URL}
|
||||
APP_BASE_URL: ${APP_BASE_URL}
|
||||
AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
|
||||
environment:
|
||||
- NGINX_PORT=${NGINX_PORT:-80}
|
||||
- WEB_URL=${WEB_URL:-http://localhost}
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
|
||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-}
|
||||
# Gunicorn Workers
|
||||
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-1}
|
||||
#DB SETTINGS
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-plane}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-plane}
|
||||
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
|
||||
# REDIS SETTINGS
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/}
|
||||
|
||||
# RabbitMQ Settings
|
||||
- RABBITMQ_HOST=${RABBITMQ_HOST:-plane-mq}
|
||||
- RABBITMQ_PORT=${RABBITMQ_PORT:-5672}
|
||||
- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER:-plane}
|
||||
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD:-plane}
|
||||
- RABBITMQ_DEFAULT_VHOST=${RABBITMQ_VHOST:-plane}
|
||||
- RABBITMQ_VHOST=${RABBITMQ_VHOST:-plane}
|
||||
- AMQP_URL=${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
|
||||
# Application secret
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
# DATA STORE SETTINGS
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- AWS_REGION=${AWS_REGION:-}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-"access-key"}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-"secret-key"}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-"access-key"}
|
||||
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
|
||||
- BUCKET_NAME=${BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
# Live server env
|
||||
- API_BASE_URL=${API_BASE_URL:-http://api:8000}
|
||||
|
||||
services:
|
||||
web:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -70,6 +61,7 @@ services:
|
||||
- worker
|
||||
|
||||
space:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -83,6 +75,7 @@ services:
|
||||
- web
|
||||
|
||||
admin:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -95,13 +88,12 @@ services:
|
||||
- web
|
||||
|
||||
live:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: node live/dist/server.js live
|
||||
environment:
|
||||
<<: [ *live-env ]
|
||||
deploy:
|
||||
replicas: ${LIVE_REPLICAS:-1}
|
||||
depends_on:
|
||||
@@ -109,6 +101,7 @@ services:
|
||||
- web
|
||||
|
||||
api:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -118,14 +111,14 @@ services:
|
||||
replicas: ${API_REPLICAS:-1}
|
||||
volumes:
|
||||
- logs_api:/code/plane/logs
|
||||
environment:
|
||||
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
|
||||
worker:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -133,8 +126,6 @@ services:
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
volumes:
|
||||
- logs_worker:/code/plane/logs
|
||||
environment:
|
||||
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
@@ -142,6 +133,7 @@ services:
|
||||
- plane-mq
|
||||
|
||||
beat-worker:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -149,8 +141,6 @@ services:
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
volumes:
|
||||
- logs_beat-worker:/code/plane/logs
|
||||
environment:
|
||||
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
@@ -158,6 +148,7 @@ services:
|
||||
- plane-mq
|
||||
|
||||
migrator:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -165,23 +156,21 @@ services:
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
- logs_migrator:/code/plane/logs
|
||||
environment:
|
||||
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-db:
|
||||
<<: *app-env
|
||||
image: postgres:15.7-alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: postgres -c 'max_connections=1000'
|
||||
environment:
|
||||
<<: *db-env
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
plane-redis:
|
||||
<<: *app-env
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
@@ -189,33 +178,30 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
plane-mq:
|
||||
<<: *app-env
|
||||
image: rabbitmq:3.13.6-management-alpine
|
||||
restart: always
|
||||
environment:
|
||||
<<: *mq-env
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
|
||||
plane-minio:
|
||||
<<: *app-env
|
||||
image: minio/minio:latest
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: server /export --console-address ":9090"
|
||||
environment:
|
||||
<<: *minio-env
|
||||
volumes:
|
||||
- uploads:/export
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
environment:
|
||||
<<: *proxy-env
|
||||
depends_on:
|
||||
- web
|
||||
- api
|
||||
|
||||
+13
-97
@@ -4,12 +4,9 @@ BRANCH=${BRANCH:-master}
|
||||
SCRIPT_DIR=$PWD
|
||||
SERVICE_FOLDER=plane-app
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
export APP_RELEASE=stable
|
||||
export APP_RELEASE="stable"
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=${PULL_POLICY:-if_not_present}
|
||||
export GH_REPO=makeplane/plane
|
||||
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
|
||||
export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deploy/selfhost"
|
||||
|
||||
CPU_ARCH=$(uname -m)
|
||||
OS_NAME=$(uname)
|
||||
@@ -19,6 +16,13 @@ mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
|
||||
SED_PREFIX=()
|
||||
if [ "$OS_NAME" == "Darwin" ]; then
|
||||
SED_PREFIX=("-i" "")
|
||||
else
|
||||
SED_PREFIX=("-i")
|
||||
fi
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
|
||||
@@ -55,17 +59,6 @@ function spinner() {
|
||||
printf " \b\b\b\b" >&2
|
||||
}
|
||||
|
||||
function checkLatestRelease(){
|
||||
echo "Checking for the latest release..." >&2
|
||||
local latest_release=$(curl -s https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g')
|
||||
if [ -z "$latest_release" ]; then
|
||||
echo "Failed to check for the latest release. Exiting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo $latest_release
|
||||
}
|
||||
|
||||
function initialize(){
|
||||
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${UPPER_CPU_ARCH} support." >&2
|
||||
|
||||
@@ -137,12 +130,8 @@ function updateEnvFile() {
|
||||
echo "$key=$value" >> "$file"
|
||||
return
|
||||
else
|
||||
if [ "$OS_NAME" == "Darwin" ]; then
|
||||
value=$(echo "$value" | sed 's/|/\\|/g')
|
||||
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
|
||||
else
|
||||
sed -i "s/^$key=.*/$key=$value/g" "$file"
|
||||
fi
|
||||
# if key exists, update the value
|
||||
sed "${SED_PREFIX[@]}" "s/^$key=.*/$key=$value/g" "$file"
|
||||
fi
|
||||
else
|
||||
echo "File not found: $file"
|
||||
@@ -193,7 +182,7 @@ function buildYourOwnImage(){
|
||||
local PLANE_TEMP_CODE_DIR=~/tmp/plane
|
||||
rm -rf $PLANE_TEMP_CODE_DIR
|
||||
mkdir -p $PLANE_TEMP_CODE_DIR
|
||||
REPO=https://github.com/$GH_REPO.git
|
||||
REPO=https://github.com/makeplane/plane.git
|
||||
git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
|
||||
|
||||
cp "$PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
|
||||
@@ -215,10 +204,6 @@ function install() {
|
||||
echo "Begin Installing Plane"
|
||||
echo ""
|
||||
|
||||
if [ "$APP_RELEASE" == "stable" ]; then
|
||||
export APP_RELEASE=$(checkLatestRelease)
|
||||
fi
|
||||
|
||||
local build_image=$(initialize)
|
||||
|
||||
if [ "$build_image" == "build" ]; then
|
||||
@@ -247,49 +232,8 @@ function download() {
|
||||
mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
|
||||
fi
|
||||
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
else
|
||||
# Fallback to download from the raw github url
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
else
|
||||
echo "Failed to download docker-compose.yml. HTTP Status: $STATUS"
|
||||
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml"
|
||||
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
|
||||
else
|
||||
# Fallback to download from the raw github url
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
|
||||
else
|
||||
echo "Failed to download variables.env. HTTP Status: $STATUS"
|
||||
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env"
|
||||
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/docker-compose.yaml https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
|
||||
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/variables-upgrade.env https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
|
||||
|
||||
if [ -f "$DOCKER_ENV_PATH" ];
|
||||
then
|
||||
@@ -391,34 +335,6 @@ function restartServices() {
|
||||
startServices
|
||||
}
|
||||
function upgrade() {
|
||||
local latest_release=$(checkLatestRelease)
|
||||
|
||||
echo ""
|
||||
echo "Current release: $APP_RELEASE"
|
||||
|
||||
if [ "$latest_release" == "$APP_RELEASE" ]; then
|
||||
echo ""
|
||||
echo "You are already using the latest release"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Latest release: $latest_release"
|
||||
echo ""
|
||||
|
||||
# Check for confirmation to upgrade
|
||||
echo "Do you want to upgrade to the latest release ($latest_release)?"
|
||||
read -p "Continue? [y/N]: " confirm
|
||||
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Exiting..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
export APP_RELEASE=$latest_release
|
||||
|
||||
echo "Upgrading Plane to the latest release..."
|
||||
echo ""
|
||||
|
||||
echo "***** STOPPING SERVICES ****"
|
||||
stopServices
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ AWS_ACCESS_KEY_ID=access-key
|
||||
AWS_SECRET_ACCESS_KEY=secret-key
|
||||
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
|
||||
AWS_S3_BUCKET_NAME=uploads
|
||||
MINIO_ROOT_USER=access-key
|
||||
MINIO_ROOT_PASSWORD=secret-key
|
||||
BUCKET_NAME=uploads
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
|
||||
# Gunicorn Workers
|
||||
|
||||
@@ -11,7 +11,6 @@ export enum EIssueGroupByToServerOptions {
|
||||
"target_date" = "target_date",
|
||||
"project" = "project_id",
|
||||
"created_by" = "created_by",
|
||||
"team_project" = "project_id",
|
||||
}
|
||||
|
||||
export enum EIssueGroupBYServerToProperty {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -1,9 +0,0 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
# Logger Package
|
||||
|
||||
This package provides a logger and a request logger utility built using [Winston](https://github.com/winstonjs/winston). It offers customizable log levels using env and supports structured logging for general application logs and HTTP requests.
|
||||
|
||||
## Features.
|
||||
- Dynamic log level configuration using env.
|
||||
- Pre-configured winston logger for general usage (`logger`).
|
||||
- Request logger middleware that logs incoming request
|
||||
|
||||
## Usage
|
||||
|
||||
### Adding as a package
|
||||
Add this package as a dependency in package.json
|
||||
```typescript
|
||||
dependency: {
|
||||
...
|
||||
@plane/logger":"*",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Importing the Logger
|
||||
```typescript
|
||||
import { logger, requestLogger } from '@plane/logger'
|
||||
```
|
||||
### Usage
|
||||
### `logger`: General Logger
|
||||
Use this for general application logs.
|
||||
|
||||
```typescript
|
||||
logger.info("This is an info log");
|
||||
logger.warn("This is a warning");
|
||||
logger.error("This is an error");
|
||||
```
|
||||
|
||||
### `requestLogger`: Request Logger Middleware
|
||||
Use this as a middleware for incoming requests
|
||||
|
||||
```typescript
|
||||
const app = express()
|
||||
app.use(requestLogger)
|
||||
```
|
||||
|
||||
## Available Log Levels
|
||||
- `error`
|
||||
- `warn`
|
||||
- `info` (default)
|
||||
- `http`
|
||||
- `verbose`
|
||||
- `debug`
|
||||
- `silly`
|
||||
|
||||
## Log file
|
||||
- Log files are stored in logs folder of current working directory. Error logs are stored in files with format `error-%DATE%.log` and combined logs are stored with format `combined-%DATE%.log`.
|
||||
- Log files have a 7 day rotation period defined.
|
||||
|
||||
## Configuration
|
||||
- By default, the log level is set to `info`.
|
||||
- You can specify a log level by adding a LOG_LEVEL in .env.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "@plane/logger",
|
||||
"version": "0.24.1",
|
||||
"description": "Logger shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
|
||||
},
|
||||
"dependencies": {
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
"@types/node": "^22.5.4",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import winston from "winston";
|
||||
import DailyRotateFile from "winston-daily-rotate-file";
|
||||
import path from "path";
|
||||
|
||||
// Define log levels
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
};
|
||||
|
||||
// Define colors for each level
|
||||
const colors = {
|
||||
error: "red",
|
||||
warn: "yellow",
|
||||
info: "green",
|
||||
http: "magenta",
|
||||
debug: "white",
|
||||
};
|
||||
|
||||
// Tell winston about our colors
|
||||
winston.addColors(colors);
|
||||
|
||||
// Custom format for logging
|
||||
const format = winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf(
|
||||
(info: winston.Logform.TransformableInfo) => `[${info?.timestamp}] ${info.level}: ${info.message}`
|
||||
)
|
||||
);
|
||||
|
||||
// Define which transports to use
|
||||
const transports = [
|
||||
// Console transport
|
||||
new winston.transports.Console(),
|
||||
|
||||
// Rotating file transport for errors
|
||||
new DailyRotateFile({
|
||||
filename: path.join(process.cwd(), "logs", "error-%DATE%.log"),
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: true,
|
||||
maxSize: process.env.LOG_MAX_SIZE || "20m",
|
||||
maxFiles: process.env.LOG_RETENTION || "7d",
|
||||
level: "error",
|
||||
}),
|
||||
|
||||
// Rotating file transport for all logs
|
||||
new DailyRotateFile({
|
||||
filename: path.join(process.cwd(), "logs", "combined-%DATE%.log"),
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: true,
|
||||
maxSize: process.env.LOG_MAX_SIZE || "20m",
|
||||
maxFiles: process.env.LOG_RETENTION || "7d",
|
||||
}),
|
||||
];
|
||||
|
||||
// Create the logger
|
||||
export const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./config";
|
||||
export * from "./middleware";
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { logger } from "./config";
|
||||
|
||||
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
||||
// Log when the request starts
|
||||
const startTime = Date.now();
|
||||
|
||||
// Log request details
|
||||
logger.http(`Incoming ${req.method} request to ${req.url} from ${req.ip}`);
|
||||
|
||||
// Log request body if present
|
||||
if (Object.keys(req.body).length > 0) {
|
||||
logger.debug("Request body:", req.body);
|
||||
}
|
||||
|
||||
// Capture response
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.http(`Completed ${req.method} ${req.url} with status ${res.statusCode} in ${duration}ms`);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"extends": "@plane/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
export type TCommandPaletteActionList = Record<
|
||||
string,
|
||||
{ title: string; description: string; action: () => void }
|
||||
>;
|
||||
|
||||
export type TCommandPaletteShortcutList = {
|
||||
key: string;
|
||||
title: string;
|
||||
shortcuts: TCommandPaletteShortcut[];
|
||||
};
|
||||
|
||||
export type TCommandPaletteShortcut = {
|
||||
keys: string; // comma separated keys
|
||||
description: string;
|
||||
};
|
||||
Vendored
-2
@@ -22,5 +22,3 @@ export type TLogoProps = {
|
||||
background_color?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
||||
|
||||
@@ -59,5 +59,4 @@ export enum EFileAssetType {
|
||||
USER_AVATAR = "USER_AVATAR",
|
||||
USER_COVER = "USER_COVER",
|
||||
WORKSPACE_LOGO = "WORKSPACE_LOGO",
|
||||
TEAM_SPACE_DESCRIPTION = "TEAM_SPACE_DESCRIPTION",
|
||||
}
|
||||
|
||||
Vendored
-1
@@ -32,4 +32,3 @@ export * from "./workspace-notifications";
|
||||
export * from "./favorite";
|
||||
export * from "./file";
|
||||
export * from "./workspace-draft-issues/base";
|
||||
export * from "./command-palette";
|
||||
|
||||
Vendored
+2
-3
@@ -211,13 +211,12 @@ export type GroupByColumnTypes =
|
||||
| "priority"
|
||||
| "labels"
|
||||
| "assignees"
|
||||
| "created_by"
|
||||
| "team_project";
|
||||
| "created_by";
|
||||
|
||||
export interface IGroupByColumn {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: ReactElement | undefined;
|
||||
icon: ReactElement | undefined;
|
||||
payload: Partial<TIssue>;
|
||||
isDropDisabled?: boolean;
|
||||
dropErrorMessage?: string;
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./project_filters";
|
||||
export * from "./projects";
|
||||
export * from "./project_link";
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
export type TProjectLinkEditableFields = {
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type TProjectLink = TProjectLinkEditableFields & {
|
||||
created_by_id: string;
|
||||
id: string;
|
||||
metadata: any;
|
||||
project_id: string;
|
||||
|
||||
//need
|
||||
created_at: Date;
|
||||
};
|
||||
|
||||
export type TProjectLinkMap = {
|
||||
[project_id: string]: TProjectLink;
|
||||
};
|
||||
|
||||
export type TProjectLinkIdMap = {
|
||||
[project_id: string]: string[];
|
||||
};
|
||||
Vendored
-3
@@ -18,7 +18,6 @@ export type TIssueGroupByOptions =
|
||||
| "cycle"
|
||||
| "module"
|
||||
| "target_date"
|
||||
| "team_project"
|
||||
| null;
|
||||
|
||||
export type TIssueOrderByOptions =
|
||||
@@ -70,7 +69,6 @@ export type TIssueParams =
|
||||
| "start_date"
|
||||
| "target_date"
|
||||
| "project"
|
||||
| "team_project"
|
||||
| "group_by"
|
||||
| "sub_group_by"
|
||||
| "order_by"
|
||||
@@ -94,7 +92,6 @@ export interface IIssueFilterOptions {
|
||||
cycle?: string[] | null;
|
||||
module?: string[] | null;
|
||||
project?: string[] | null;
|
||||
team_project?: string[] | null;
|
||||
start_date?: string[] | null;
|
||||
state?: string[] | null;
|
||||
state_group?: string[] | null;
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export type TNotificationData = {
|
||||
};
|
||||
|
||||
export type TNotification = {
|
||||
id: string;
|
||||
id: string | undefined;
|
||||
title: string | undefined;
|
||||
data: TNotificationData | undefined;
|
||||
entity_identifier: string | undefined;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
import { join, dirname } from "path";
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string): any {
|
||||
return dirname(require.resolve(join(value, "package.json")));
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-onboarding"),
|
||||
getAbsolutePath("@storybook/addon-essentials"),
|
||||
getAbsolutePath("@chromatic-com/storybook"),
|
||||
getAbsolutePath("@storybook/addon-interactions"),
|
||||
],
|
||||
framework: {
|
||||
name: getAbsolutePath("@storybook/react-vite"),
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
<div id="portal-root"></div>
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "../styles/output.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,94 @@
|
||||
export const fruits = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Apple",
|
||||
emoji: "🍎",
|
||||
description:
|
||||
"A sweet and crisp fruit, often red or green, great for snacking and baking.",
|
||||
// disabled: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Banana",
|
||||
emoji: "🍌",
|
||||
description:
|
||||
"A long and curved fruit with a soft, creamy flesh, known for its energy-boosting properties.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Cherry",
|
||||
emoji: "🍒",
|
||||
description:
|
||||
"Small, round, and juicy fruits with a sweet or tart flavor, often used in desserts.",
|
||||
// disabled: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Grapes",
|
||||
emoji: "🍇",
|
||||
description:
|
||||
"Small, juicy fruits that grow in clusters, available in various colors like green, red, and purple.",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Orange",
|
||||
emoji: "🍊",
|
||||
description:
|
||||
"A citrus fruit known for its tangy and refreshing taste, rich in vitamin C.",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Strawberry",
|
||||
emoji: "🍓",
|
||||
description:
|
||||
"A red, heart-shaped fruit with a sweet flavor and tiny seeds on its surface.",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Watermelon",
|
||||
emoji: "🍉",
|
||||
description:
|
||||
"A large, juicy fruit with green rind and red flesh, perfect for summertime snacks.",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Peach",
|
||||
emoji: "🍑",
|
||||
description:
|
||||
"A soft, fuzzy fruit with a sweet and slightly tangy taste, often enjoyed fresh or in desserts.",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Pineapple",
|
||||
emoji: "🍍",
|
||||
description:
|
||||
"A tropical fruit with spiky skin and sweet, tangy yellow flesh.",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Lemon",
|
||||
emoji: "🍋",
|
||||
description:
|
||||
"A bright yellow citrus fruit with a tart flavor, commonly used in drinks and cooking.",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Mango",
|
||||
emoji: "🥭",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Gooseberry",
|
||||
emoji: "🍇",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Grapefruit",
|
||||
emoji: "🍊",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Guava",
|
||||
emoji: "🍈",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,73 @@
|
||||
export const vegetables = [
|
||||
// Add 10 vegetables
|
||||
{
|
||||
id: 12,
|
||||
name: "Carrot",
|
||||
emoji: "🥕",
|
||||
description:
|
||||
"A crunchy orange root vegetable, rich in beta-carotene and vitamin A.",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Broccoli",
|
||||
emoji: "🥦",
|
||||
description:
|
||||
"A green vegetable with dense, nutritious florets, high in fiber and vitamins.",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Tomato",
|
||||
emoji: "🍅",
|
||||
description:
|
||||
"Technically a fruit, but commonly used as a vegetable in cooking.",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Eggplant",
|
||||
emoji: "🍆",
|
||||
description:
|
||||
"A purple vegetable with a meaty texture, popular in Mediterranean cuisine.",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Corn",
|
||||
emoji: "🌽",
|
||||
description:
|
||||
"Sweet yellow kernels on a cob, enjoyed grilled, boiled, or popped.",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Bell Pepper",
|
||||
emoji: "🫑",
|
||||
description:
|
||||
"A crisp, colorful vegetable that can be sweet or slightly bitter.",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Cucumber",
|
||||
emoji: "🥒",
|
||||
description:
|
||||
"A refreshing green vegetable with high water content, often used in salads.",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Potato",
|
||||
emoji: "🥔",
|
||||
description:
|
||||
"A starchy root vegetable that can be prepared in countless ways.",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mushroom",
|
||||
emoji: "🍄",
|
||||
description:
|
||||
"Technically a fungus, but commonly used as a vegetable in cooking.",
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: "Onion",
|
||||
emoji: "🧅",
|
||||
description:
|
||||
"A pungent bulb vegetable used as a base in many cuisines worldwide.",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
testEnvironment: "jsdom",
|
||||
moduleNameMapper: {
|
||||
".(css|less|scss)$": "identity-obj-proxy",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@plane/ui-v2",
|
||||
"version": "1.0.0",
|
||||
"description": "Revamped UI components built using Radix, shared across multiple apps internally",
|
||||
"author": "Plane",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm --dts --external react --minify",
|
||||
"dev": "tsup src/index.ts --format esm --watch --dts --external react",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"postcss": "postcss styles/globals.css -o styles/output.css --watch",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"tcss": "tailwindcss -i ./styles/global.css -o ./styles/output.css --watch",
|
||||
"start": "yarn dev & yarn storybook & yarn tcss",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.2",
|
||||
"@storybook/addon-essentials": "^8.4.5",
|
||||
"@storybook/addon-interactions": "^8.4.5",
|
||||
"@storybook/addon-onboarding": "^8.4.5",
|
||||
"@storybook/blocks": "^8.4.5",
|
||||
"@storybook/react": "^8.4.5",
|
||||
"@storybook/react-vite": "^8.4.5",
|
||||
"@storybook/test": "^8.4.5",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@vitest/ui": "2.1.8",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"storybook": "^8.4.5",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.1",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-select": "^2.1.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
## Philosophy
|
||||
|
||||
Components are our core building blocks adhering to our design system. None of these components should ever need any styling in the application.
|
||||
|
||||
Only styles you will need in the application are for layouts.
|
||||
|
||||
## Composition vs Render props
|
||||
|
||||
For any data that needs to be dynamic / comes from the server, we use render props.
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { fn } from "@storybook/test";
|
||||
import React, { useState } from "react";
|
||||
import { DropdownMenu } from "./DropdownMenu";
|
||||
import { DropdownButton } from "./components/DropdownButton";
|
||||
import { DropdownContent } from "./components/DropdownContent";
|
||||
import { DropdownItem } from "./components/DropdownItem";
|
||||
import { fruits } from "../../../data/fruits";
|
||||
import { vegetables } from "../../../data/vegetables";
|
||||
|
||||
// import { SelectDropdown } from "./DropdownMenu-copy";
|
||||
|
||||
const fruitsAndVegetables = {
|
||||
fruits,
|
||||
vegetables,
|
||||
};
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||
const meta = {
|
||||
title: "Example/DropdownMenu",
|
||||
component: DropdownMenu,
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: "centered",
|
||||
},
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ["autodocs"],
|
||||
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||
argTypes: {
|
||||
items: [1, 2, 3, 4],
|
||||
},
|
||||
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
|
||||
args: {},
|
||||
} satisfies Meta<typeof DropdownMenu>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const DefaultDropdown = () => {
|
||||
return (
|
||||
<DropdownMenu defaultOpen={true}>
|
||||
<DropdownButton>
|
||||
<button>Click me!!</button>
|
||||
</DropdownButton>
|
||||
<DropdownContent>
|
||||
<div>
|
||||
<h1>Hello</h1>
|
||||
<p>How are you today?</p>
|
||||
<DropdownItem onSelect={(e) => console.log(e)}>
|
||||
Click me again
|
||||
</DropdownItem>
|
||||
<DropdownItem onSelect={(e) => e.preventDefault()}>
|
||||
Click me, I won't close
|
||||
</DropdownItem>
|
||||
</div>
|
||||
</DropdownContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||
export const MultiSelect = () => {
|
||||
const [value, setValue] = useState([fruits[6]]);
|
||||
const [items, setItems] = useState([
|
||||
...fruitsAndVegetables.fruits,
|
||||
...fruitsAndVegetables.vegetables,
|
||||
]);
|
||||
|
||||
const handleSearch = async (query: String) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// Filters items on the name
|
||||
const filteredItems = fruitsAndVegetables.fruits.filter((item) =>
|
||||
item.name.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
setItems(filteredItems);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
onSelect={(e, value) => {
|
||||
e.preventDefault();
|
||||
console.log(e, value);
|
||||
}}
|
||||
renderItem={(item) => <Fruit fruit={item} />}
|
||||
defaultOpen={true}
|
||||
onSearch={handleSearch}
|
||||
// isItemDisabled={(item) => item.id % 2 === 0}
|
||||
>
|
||||
<DropdownButton showIcon>
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<Fruit fruit={fruits[1]} />({value.length})
|
||||
</div>
|
||||
</DropdownButton>
|
||||
|
||||
{items.length === 0 && (
|
||||
<DropdownContent>
|
||||
<div>No items found</div>
|
||||
</DropdownContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export const NestedDropdown = () => {
|
||||
const items = [
|
||||
{
|
||||
name: "Vegetables",
|
||||
children: fruitsAndVegetables.vegetables,
|
||||
emoji: "🥦",
|
||||
},
|
||||
{
|
||||
name: "Fruits",
|
||||
children: fruitsAndVegetables.fruits,
|
||||
emoji: "🍎",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
<DropdownMenu
|
||||
defaultOpen={true}
|
||||
items={items}
|
||||
renderItem={(item) => <Fruit fruit={item} />}
|
||||
>
|
||||
<DropdownButton>
|
||||
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
Select
|
||||
</button>
|
||||
</DropdownButton>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const Fruit = ({ fruit }) => {
|
||||
return <div>{`${fruit.emoji} ${fruit.name}`}</div>;
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DropdownButton } from "./components/DropdownButton";
|
||||
import { DropdownMenu } from "./DropdownMenu";
|
||||
|
||||
describe("DropdownMenu", () => {
|
||||
it("should render the dropdown menu", () => {
|
||||
render(
|
||||
<DropdownMenu>
|
||||
<DropdownButton>Click me</DropdownButton>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText("Click me")).toHaveTextContent("Click me");
|
||||
});
|
||||
|
||||
it("should render the dropdown arrow", () => {
|
||||
render(
|
||||
<DropdownMenu>
|
||||
<DropdownButton showIcon>Click me</DropdownButton>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByTestId("dropdown-arrow")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Should not render the dropdown arrow if showIcon is false", () => {
|
||||
render(
|
||||
<DropdownMenu>
|
||||
<DropdownButton>Click me</DropdownButton>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.queryByTestId("dropdown-arrow")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render items using render prop", async () => {
|
||||
type Item = { id: number; label: string; disabled?: boolean };
|
||||
const items: Item[] = [
|
||||
{ id: 1, label: "Item 1" },
|
||||
{ id: 2, label: "Item 2", disabled: true },
|
||||
];
|
||||
|
||||
const onSelectMock = vi.fn();
|
||||
|
||||
render(
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
onSelect={onSelectMock}
|
||||
renderItem={(item) => <div>{item.label}</div>}
|
||||
defaultOpen
|
||||
>
|
||||
<DropdownButton>Click me</DropdownButton>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
// Check if items are rendered
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
||||
|
||||
// Check if disabled item has correct attributes
|
||||
const disabledItem = screen.getByText("Item 2");
|
||||
expect(disabledItem.parentElement).toHaveAttribute("data-disabled");
|
||||
|
||||
// Check if the item is clickable and triggers onSelect
|
||||
const activeItem = screen.getByText("Item 1");
|
||||
act(() => {
|
||||
fireEvent(activeItem, new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onSelectMock).toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Item 1")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import React, { createContext, useCallback, useState } from "react";
|
||||
import { DropdownContent } from "./components/DropdownContent";
|
||||
|
||||
// Types
|
||||
type DropdownMenuProps<T> = {
|
||||
children: React.ReactNode;
|
||||
items?: T[];
|
||||
onSelect?: (e: React.MouseEvent, value: T) => void;
|
||||
renderItem?: (item: T) => React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
onSearch?: (value: string) => void;
|
||||
isItemDisabled?: (item: T) => boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
type DropdownMenuContextType<T> = {
|
||||
items?: T[];
|
||||
onSelect?: (e: React.MouseEvent, value: T) => void;
|
||||
renderItem?: (item: T) => React.ReactNode;
|
||||
setOpen?: (open: boolean) => void;
|
||||
onSearch?: (value: string) => void;
|
||||
isItemDisabled?: (item: T) => boolean;
|
||||
};
|
||||
|
||||
//@todo: Is it possible to not use any here?
|
||||
export const DropdownMenuContext = createContext<DropdownMenuContextType<any>>({
|
||||
items: [],
|
||||
onSelect: (e, value) => {},
|
||||
renderItem: (item) => <></>,
|
||||
});
|
||||
|
||||
export const DropdownMenu = <T,>({
|
||||
children,
|
||||
items,
|
||||
onSelect,
|
||||
renderItem,
|
||||
defaultOpen = false,
|
||||
onSearch,
|
||||
isItemDisabled,
|
||||
onOpenChange,
|
||||
}: DropdownMenuProps<T>) => {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
if (onOpenChange) onOpenChange(open);
|
||||
setTimeout(() => {
|
||||
setOpen(open);
|
||||
}, 16);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DropdownMenuContext.Provider
|
||||
value={{ items, onSelect, renderItem, setOpen, onSearch, isItemDisabled }}
|
||||
>
|
||||
<RadixDropdownMenu.Root open={open} onOpenChange={handleOpenChange}>
|
||||
{children}
|
||||
{items && <DropdownContent />}
|
||||
</RadixDropdownMenu.Root>
|
||||
</DropdownMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
||||
import React, { useContext } from "react";
|
||||
import { DropdownMenuContext } from "../DropdownMenu";
|
||||
|
||||
type DropdownButtonProps = {
|
||||
children: React.ReactNode;
|
||||
showIcon?: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const DropdownButton = ({
|
||||
children,
|
||||
showIcon = false,
|
||||
label,
|
||||
}: DropdownButtonProps) => {
|
||||
const { setOpen } = useContext(DropdownMenuContext);
|
||||
|
||||
return (
|
||||
<RadixDropdownMenu.Trigger
|
||||
className="inline-flex h-[35px]
|
||||
items-center justify-between gap-[5px] rounded bg-white px-[15px] text-[13px] leading-none"
|
||||
aria-label={label}
|
||||
asChild
|
||||
>
|
||||
<div>
|
||||
{children}
|
||||
{showIcon && <ChevronDownIcon data-testid="dropdown-arrow" />}
|
||||
</div>
|
||||
</RadixDropdownMenu.Trigger>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import React from "react";
|
||||
import { DropdownMenu } from "../DropdownMenu";
|
||||
import { DropdownContent } from "./DropdownContent";
|
||||
import { DropdownButton } from "./DropdownButton";
|
||||
|
||||
let portalRoot = document.getElementById("portal");
|
||||
if (!portalRoot) {
|
||||
portalRoot = document.createElement("div");
|
||||
portalRoot.setAttribute("id", "portal");
|
||||
document.body.appendChild(portalRoot);
|
||||
console.log("Added");
|
||||
}
|
||||
|
||||
const Item = ({ item }: { item: any }) => {
|
||||
return <div>{item.label}</div>;
|
||||
};
|
||||
describe("DropdownContent", () => {
|
||||
it("should render the dropdown content", () => {
|
||||
render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownButton>Click me</DropdownButton>
|
||||
<DropdownContent
|
||||
container={document.getElementById("portal") || undefined}
|
||||
>
|
||||
Content.........
|
||||
</DropdownContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByRole("menu")).toHaveTextContent("Content");
|
||||
});
|
||||
|
||||
it("should render dropdown items", () => {
|
||||
const items = [
|
||||
{ label: "Item 1", value: "item-1" },
|
||||
{ label: "Item 2", value: "item-2" },
|
||||
];
|
||||
render(
|
||||
<DropdownMenu
|
||||
defaultOpen
|
||||
items={items}
|
||||
renderItem={(item) => <Item item={item} />}
|
||||
>
|
||||
<DropdownButton>Click me</DropdownButton>
|
||||
<DropdownContent></DropdownContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
console.log(screen.debug());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { DropdownMenuContext } from "../DropdownMenu";
|
||||
import { DropdownItems } from "./DropdownItems";
|
||||
|
||||
export const DropdownContent = ({
|
||||
children,
|
||||
container,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
container?: HTMLElement;
|
||||
}) => {
|
||||
const { items, renderItem, onSelect, renderGroup, onSearch, isItemDisabled } =
|
||||
useContext(DropdownMenuContext);
|
||||
|
||||
const [groupedItems, setGroupedItems] = useState<{
|
||||
[key: string]: any[];
|
||||
}>({});
|
||||
|
||||
const [showSearchLoading, setShowSearchLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!items) return;
|
||||
let groupedItems: { [key: string]: any[] } = {};
|
||||
if (Array.isArray(items)) {
|
||||
groupedItems.default = items;
|
||||
} else {
|
||||
groupedItems = items;
|
||||
}
|
||||
|
||||
setGroupedItems(groupedItems);
|
||||
}, [open, items]);
|
||||
|
||||
const handleSearch = async (query: string) => {
|
||||
if (!onSearch) return;
|
||||
try {
|
||||
setShowSearchLoading(true);
|
||||
await onSearch(query);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setShowSearchLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(e: any, item: any) => {
|
||||
onSelect && onSelect(e, item);
|
||||
},
|
||||
[onSelect]
|
||||
);
|
||||
|
||||
return (
|
||||
<RadixDropdownMenu.Portal container={container}>
|
||||
<RadixDropdownMenu.Content className="p-3 rounded-md bg-white border border-neutral">
|
||||
{onSearch && (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="w-full p-1 border border-neutral rounded-md mb-3 sticky top-0 bg-white z-10"
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
{showSearchLoading && (
|
||||
<div role="status" className="absolute z-20 top-2 right-2">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-4 h-4 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ScrollArea.Root className=" ">
|
||||
<ScrollArea.Viewport className="h-full w-full max-h-[80vh]">
|
||||
{children}
|
||||
|
||||
{Object.keys(groupedItems).map((group) => (
|
||||
<RadixDropdownMenu.Group key={group}>
|
||||
{group !== "default" && (
|
||||
<RadixDropdownMenu.Label>
|
||||
{renderGroup ? renderGroup(group) : group}
|
||||
</RadixDropdownMenu.Label>
|
||||
)}
|
||||
{renderItem && (
|
||||
<DropdownItems
|
||||
items={groupedItems[group]}
|
||||
onSelect={handleSelect}
|
||||
renderItem={renderItem}
|
||||
isItemDisabled={isItemDisabled}
|
||||
/>
|
||||
)}
|
||||
</RadixDropdownMenu.Group>
|
||||
))}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
className="flex touch-none select-none bg-blackA3 p-0.5 transition-colors duration-[160ms] ease-out hover:bg-blackA5 data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col"
|
||||
orientation="vertical"
|
||||
>
|
||||
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-mauve10 before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-11 before:min-w-11 before:-translate-x-1/2 before:-translate-y-1/2" />
|
||||
</ScrollArea.Scrollbar>
|
||||
<ScrollArea.Scrollbar
|
||||
className="flex touch-none select-none bg-blackA3 p-0.5 transition-colors duration-[160ms] ease-out hover:bg-blackA5 data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col"
|
||||
orientation="horizontal"
|
||||
>
|
||||
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-mauve10 before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-[44px] before:min-w-[44px] before:-translate-x-1/2 before:-translate-y-1/2" />
|
||||
</ScrollArea.Scrollbar>
|
||||
<ScrollArea.Corner className="bg-blackA5" />
|
||||
</ScrollArea.Root>
|
||||
</RadixDropdownMenu.Content>
|
||||
</RadixDropdownMenu.Portal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
const DropdownItem_ = ({
|
||||
children,
|
||||
onSelect,
|
||||
disabled,
|
||||
item,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onSelect: (e: any, item: any) => void;
|
||||
disabled?: boolean;
|
||||
item?: any;
|
||||
}) => {
|
||||
const handleSelect = useCallback(
|
||||
(e: any) => {
|
||||
onSelect(e, item);
|
||||
},
|
||||
[onSelect]
|
||||
);
|
||||
return (
|
||||
<RadixDropdownMenu.Item
|
||||
onSelect={handleSelect}
|
||||
className="p-1 first:pt-0 last:border-b-0 last:pb-0
|
||||
rounded
|
||||
hover:bg-bg-neutral-subtle
|
||||
focus:bg-bg-neutral-subtle
|
||||
focus:outline-none
|
||||
cursor-pointer
|
||||
|
||||
data-[disabled]:pointer-events-none
|
||||
data-[disabled]:opacity-50
|
||||
data-[disabled]:cursor-not-allowed
|
||||
"
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</RadixDropdownMenu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const DropdownItem = React.memo(DropdownItem_);
|
||||
@@ -0,0 +1,86 @@
|
||||
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import React, { useCallback } from "react";
|
||||
import { DropdownItem } from "./DropdownItem";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
|
||||
type Item<T> = T & {
|
||||
children?: Item<T>[];
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type DropdownItemsProps<T> = {
|
||||
items: Item<T>[];
|
||||
onSelect: (e: any, item: Item<T>) => void;
|
||||
renderItem: (item: Item<T>) => React.ReactNode;
|
||||
isItemDisabled?: (item: Item<T>) => boolean;
|
||||
};
|
||||
|
||||
const DropdownItems_ = <T,>({
|
||||
items,
|
||||
onSelect,
|
||||
renderItem,
|
||||
isItemDisabled,
|
||||
}: DropdownItemsProps<T>) => {
|
||||
const handleSelect = useCallback(
|
||||
(e: any, item: Item<T>) => {
|
||||
onSelect(e, item);
|
||||
},
|
||||
[onSelect]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => {
|
||||
if (item.children) {
|
||||
return (
|
||||
<RadixDropdownMenu.Sub key={index}>
|
||||
<RadixDropdownMenu.SubTrigger
|
||||
className="group relative flex h-[25px] select-none items-center rounded-[3px] pr-[5px] text-[13px]
|
||||
leading-none text-violet11 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9
|
||||
data-[highlighted]:data-[state=open]:bg-violet9 data-[state=open]:bg-violet4 data-[disabled]:text-mauve8 data-[highlighted]:data-[state=open]:text-violet1 data-[highlighted]:text-violet1 data-[state=open]:text-violet11"
|
||||
>
|
||||
{renderItem(item)}
|
||||
</RadixDropdownMenu.SubTrigger>
|
||||
<RadixDropdownMenu.Portal>
|
||||
<RadixDropdownMenu.SubContent
|
||||
className="min-w-[220px] rounded-md bg-white p-[5px]
|
||||
shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)]
|
||||
will-change-[opacity,transform] data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade
|
||||
data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade
|
||||
"
|
||||
sideOffset={2}
|
||||
alignOffset={-5}
|
||||
>
|
||||
<ScrollArea.Root className=" ">
|
||||
<ScrollArea.Viewport className="h-full w-full max-h-[80vh]">
|
||||
<DropdownItems
|
||||
items={item.children}
|
||||
onSelect={handleSelect}
|
||||
renderItem={renderItem}
|
||||
isItemDisabled={isItemDisabled}
|
||||
/>
|
||||
</ScrollArea.Viewport>
|
||||
</ScrollArea.Root>
|
||||
</RadixDropdownMenu.SubContent>
|
||||
</RadixDropdownMenu.Portal>
|
||||
</RadixDropdownMenu.Sub>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DropdownItem
|
||||
key={index}
|
||||
onSelect={handleSelect}
|
||||
disabled={isItemDisabled ? isItemDisabled(item) : item?.disabled}
|
||||
item={item}
|
||||
>
|
||||
{renderItem(item)}
|
||||
</DropdownItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownItems = React.memo(DropdownItems_);
|
||||
|
||||
export { DropdownItems };
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { fn } from "@storybook/test";
|
||||
import React, { useState } from "react";
|
||||
import { Select } from "./Select";
|
||||
import { fruits } from "../../../data/fruits";
|
||||
import { SelectButton } from "./SelectButton";
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||
const meta = {
|
||||
title: "Example/SelectMenu",
|
||||
component: Select,
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: "centered",
|
||||
},
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ["autodocs"],
|
||||
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||
argTypes: {
|
||||
items: [1, 2, 3, 4],
|
||||
},
|
||||
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
|
||||
args: { onChange: fn() },
|
||||
} satisfies Meta<typeof Select>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||
export const SingleSelect = () => {
|
||||
const [items, setItems] = useState(fruits);
|
||||
const handleSearch = async (query: String) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// Filters items on the name
|
||||
const filteredItems = fruits.filter((item) =>
|
||||
item.name.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
setItems(filteredItems);
|
||||
};
|
||||
return (
|
||||
<Select
|
||||
items={fruits}
|
||||
renderItem={Fruit}
|
||||
onChange={function (value: any): void {
|
||||
console.log("Selected items", value);
|
||||
}}
|
||||
value={[3, 7, 9]}
|
||||
renderGroup={function (group: string): React.ReactNode {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
multiple
|
||||
defaultOpen
|
||||
>
|
||||
<SelectButton>Click Me!!</SelectButton>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
const Fruit = ({ item: fruit }) => {
|
||||
return <div>{`${fruit.emoji} ${fruit.name}`}</div>;
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { DropdownMenu } from "../DropdownMenu/DropdownMenu";
|
||||
import { SelectItem } from "./SelectItem";
|
||||
|
||||
type SelectProps = {
|
||||
items: any[];
|
||||
onChange: (value: any) => void;
|
||||
value: any;
|
||||
renderItem: (item: any) => React.ReactNode;
|
||||
renderGroup: (group: string) => React.ReactNode;
|
||||
multiple?: boolean;
|
||||
keyExtractor?: (item: any) => string;
|
||||
showSelectedAtTop?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
children: React.ReactNode;
|
||||
onSearch?: (query: String) => void;
|
||||
};
|
||||
|
||||
export const Select = (props: SelectProps) => {
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
renderItem: Item,
|
||||
renderGroup,
|
||||
multiple,
|
||||
value: initialValue = [],
|
||||
keyExtractor = (item) => item.id,
|
||||
showSelectedAtTop = true,
|
||||
defaultOpen = false,
|
||||
children,
|
||||
onSearch,
|
||||
} = props;
|
||||
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
const [open, setOpen] = React.useState(defaultOpen);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(e: React.MouseEvent, item: any) => {
|
||||
const key = keyExtractor(item);
|
||||
if (multiple) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// If values includes item, remove it
|
||||
if (value.includes(key)) {
|
||||
setValue(value.filter((v) => v !== key));
|
||||
} else {
|
||||
setValue([...value, key]);
|
||||
}
|
||||
onChange([...value, key]);
|
||||
} else {
|
||||
setValue([key]);
|
||||
onChange([key]);
|
||||
}
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const renderItem_ = useCallback(
|
||||
(item: any) => {
|
||||
const key = keyExtractor(item);
|
||||
const selected = value.includes(key);
|
||||
return (
|
||||
<SelectItem item={item} selected={selected} render={Item}></SelectItem>
|
||||
);
|
||||
},
|
||||
[value, keyExtractor]
|
||||
);
|
||||
|
||||
const orderedItems = React.useMemo(() => {
|
||||
if (showSelectedAtTop && open) {
|
||||
// Move selected items to the top
|
||||
return [
|
||||
...items.filter((item) => value.includes(keyExtractor(item))),
|
||||
...items.filter((item) => !value.includes(keyExtractor(item))),
|
||||
];
|
||||
}
|
||||
return items;
|
||||
}, [initialValue, open]);
|
||||
|
||||
const handleOpenChange = useCallback((open_: boolean) => {
|
||||
setOpen(open_);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
defaultOpen={defaultOpen}
|
||||
items={orderedItems}
|
||||
renderItem={renderItem_}
|
||||
onSelect={handleSelect}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DropdownButton } from "../DropdownMenu/components/DropdownButton";
|
||||
|
||||
export const SelectButton = DropdownButton;
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* SelectItem
|
||||
* @description Takes render function, item, and if its selected as a prop
|
||||
* @param {Function} render
|
||||
* @param {Object} item
|
||||
* @param {Boolean} selected
|
||||
*/
|
||||
|
||||
import { CheckIcon } from "@radix-ui/react-icons";
|
||||
import React from "react";
|
||||
|
||||
const EmptyIcon = () => (
|
||||
<div style={{ width: "15px", height: "15px" }}> </div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
item: any;
|
||||
selected: boolean;
|
||||
render: any;
|
||||
};
|
||||
const SelectItem_ = ({ render: Item, item, selected }: Props) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<Item item={item} />
|
||||
{/* Added empty icon to reserve space */}
|
||||
{selected ? <CheckIcon /> : <EmptyIcon />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectItem = React.memo(SelectItem_);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,63 @@
|
||||
import{R as e,r as f}from"./index-Cqyox1Tj.js";import{D as i,a as d,b as C,c as b,f as m}from"./fruits-CZbL9byb.js";import"./index-DtqWk5t9.js";import"./index-DGXSSr1l.js";const x=[{id:12,name:"Carrot",emoji:"🥕",description:"A crunchy orange root vegetable, rich in beta-carotene and vitamin A."},{id:13,name:"Broccoli",emoji:"🥦",description:"A green vegetable with dense, nutritious florets, high in fiber and vitamins."},{id:14,name:"Tomato",emoji:"🍅",description:"Technically a fruit, but commonly used as a vegetable in cooking."},{id:15,name:"Eggplant",emoji:"🍆",description:"A purple vegetable with a meaty texture, popular in Mediterranean cuisine."},{id:16,name:"Corn",emoji:"🌽",description:"Sweet yellow kernels on a cob, enjoyed grilled, boiled, or popped."},{id:17,name:"Bell Pepper",emoji:"🫑",description:"A crisp, colorful vegetable that can be sweet or slightly bitter."},{id:18,name:"Cucumber",emoji:"🥒",description:"A refreshing green vegetable with high water content, often used in salads."},{id:19,name:"Potato",emoji:"🥔",description:"A starchy root vegetable that can be prepared in countless ways."},{id:20,name:"Mushroom",emoji:"🍄",description:"Technically a fungus, but commonly used as a vegetable in cooking."},{id:21,name:"Onion",emoji:"🧅",description:"A pungent bulb vegetable used as a base in many cuisines worldwide."}],s={fruits:m,vegetables:x},O={title:"Example/DropdownMenu",component:i,parameters:{layout:"centered"},tags:["autodocs"],argTypes:{items:[1,2,3,4]},args:{}},o=()=>e.createElement(i,{defaultOpen:!0},e.createElement(d,null,e.createElement("button",null,"Click me!!")),e.createElement(C,null,e.createElement("div",null,e.createElement("h1",null,"Hello"),e.createElement("p",null,"How are you today?"),e.createElement(b,{onSelect:t=>console.log(t)},"Click me again"),e.createElement(b,{onSelect:t=>t.preventDefault()},"Click me, I won't close")))),r=()=>{const[t,p]=f.useState([m[6]]),[g,k]=f.useState([...s.fruits,...s.vegetables]),A=async n=>{await new Promise(u=>setTimeout(u,1e3));const l=s.fruits.filter(u=>u.name.toLowerCase().includes(n.toLowerCase()));k(l)};return e.createElement(i,{items:g,onSelect:(n,l)=>{n.preventDefault(),console.log(n,l)},renderItem:n=>e.createElement(c,{fruit:n}),defaultOpen:!0,onSearch:A},e.createElement(d,{showIcon:!0},e.createElement("div",{className:"flex items-center gap-2 justify-between"},e.createElement(c,{fruit:m[1]}),"(",t.length,")")),g.length===0&&e.createElement(C,null,e.createElement("div",null,"No items found")))},a=()=>{const t=[{name:"Vegetables",children:s.vegetables,emoji:"🥦"},{name:"Fruits",children:s.fruits,emoji:"🍎"}];return e.createElement("div",null,e.createElement(i,{defaultOpen:!0,items:t,renderItem:p=>e.createElement(c,{fruit:p})},e.createElement(d,null,e.createElement("button",{className:"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"},"Select"))))},c=({fruit:t})=>e.createElement("div",null,`${t.emoji} ${t.name}`);o.__docgenInfo={description:"",methods:[],displayName:"DefaultDropdown"};r.__docgenInfo={description:"",methods:[],displayName:"MultiSelect"};a.__docgenInfo={description:"",methods:[],displayName:"NestedDropdown"};var w,h,v;o.parameters={...o.parameters,docs:{...(w=o.parameters)==null?void 0:w.docs,source:{originalSource:`() => {
|
||||
return <DropdownMenu defaultOpen={true}>
|
||||
<DropdownButton>
|
||||
<button>Click me!!</button>
|
||||
</DropdownButton>
|
||||
<DropdownContent>
|
||||
<div>
|
||||
<h1>Hello</h1>
|
||||
<p>How are you today?</p>
|
||||
<DropdownItem onSelect={e => console.log(e)}>
|
||||
Click me again
|
||||
</DropdownItem>
|
||||
<DropdownItem onSelect={e => e.preventDefault()}>
|
||||
Click me, I won't close
|
||||
</DropdownItem>
|
||||
</div>
|
||||
</DropdownContent>
|
||||
</DropdownMenu>;
|
||||
}`,...(v=(h=o.parameters)==null?void 0:h.docs)==null?void 0:v.source}}};var D,y,E;r.parameters={...r.parameters,docs:{...(D=r.parameters)==null?void 0:D.docs,source:{originalSource:`() => {
|
||||
const [value, setValue] = useState([fruits[6]]);
|
||||
const [items, setItems] = useState([...fruitsAndVegetables.fruits, ...fruitsAndVegetables.vegetables]);
|
||||
const handleSearch = async (query: String) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// Filters items on the name
|
||||
const filteredItems = fruitsAndVegetables.fruits.filter(item => item.name.toLowerCase().includes(query.toLowerCase()));
|
||||
setItems(filteredItems);
|
||||
};
|
||||
return <DropdownMenu items={items} onSelect={(e, value) => {
|
||||
e.preventDefault();
|
||||
console.log(e, value);
|
||||
}} renderItem={item => <Fruit fruit={item} />} defaultOpen={true} onSearch={handleSearch}
|
||||
// isItemDisabled={(item) => item.id % 2 === 0}
|
||||
>
|
||||
<DropdownButton showIcon>
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<Fruit fruit={fruits[1]} />({value.length})
|
||||
</div>
|
||||
</DropdownButton>
|
||||
|
||||
{items.length === 0 && <DropdownContent>
|
||||
<div>No items found</div>
|
||||
</DropdownContent>}
|
||||
</DropdownMenu>;
|
||||
}`,...(E=(y=r.parameters)==null?void 0:y.docs)==null?void 0:E.source}}};var S,I,j;a.parameters={...a.parameters,docs:{...(S=a.parameters)==null?void 0:S.docs,source:{originalSource:`() => {
|
||||
const items = [{
|
||||
name: "Vegetables",
|
||||
children: fruitsAndVegetables.vegetables,
|
||||
emoji: "🥦"
|
||||
}, {
|
||||
name: "Fruits",
|
||||
children: fruitsAndVegetables.fruits,
|
||||
emoji: "🍎"
|
||||
}];
|
||||
return <div>
|
||||
<DropdownMenu defaultOpen={true} items={items} renderItem={item => <Fruit fruit={item} />}>
|
||||
<DropdownButton>
|
||||
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
Select
|
||||
</button>
|
||||
</DropdownButton>
|
||||
</DropdownMenu>
|
||||
</div>;
|
||||
}`,...(j=(I=a.parameters)==null?void 0:I.docs)==null?void 0:j.source}}};const _=["DefaultDropdown","MultiSelect","NestedDropdown"];export{o as DefaultDropdown,r as MultiSelect,a as NestedDropdown,_ as __namedExportsOrder,O as default};
|
||||
@@ -0,0 +1,9 @@
|
||||
import{f as x}from"./index-B5ZI-g0m.js";import{R as t,r as y}from"./index-Cqyox1Tj.js";import{C as O,D as _,a as b,f as M}from"./fruits-CZbL9byb.js";import"./index-DtqWk5t9.js";import"./index-DGXSSr1l.js";const B=()=>t.createElement("div",{style:{width:"15px",height:"15px"}}," "),l=a=>{const{items:i,onChange:m,renderItem:R,renderGroup:G,multiple:w,value:d=[],keyExtractor:o=e=>e.id,showSelectedAtTop:E=!0,defaultOpen:p=!1,children:v}=a,[n,c]=t.useState(d),[g,C]=t.useState(p),T=(e,u)=>{const r=o(u);w?(e.preventDefault(),e.stopPropagation(),n.includes(r)?c(n.filter(k=>k!==r)):c([...n,r]),m([...n,r])):(c([r]),m([r]))},I=y.useCallback(e=>{const u=o(e),r=n.includes(u);return t.createElement("div",{className:"flex items-center gap-2 justify-between"},t.createElement(R,{fruit:e}),r?t.createElement(O,null):t.createElement(B,null))},[n,o]),N=t.useMemo(()=>E&&g?[...i.filter(e=>n.includes(o(e))),...i.filter(e=>!n.includes(o(e)))]:i,[d,g]),q=y.useCallback(e=>{C(e)},[]);return t.createElement(_,{defaultOpen:p,items:N,renderItem:I,onSelect:T,onOpenChange:q},v)};l.__docgenInfo={description:"",methods:[],displayName:"Select",props:{items:{required:!0,tsType:{name:"Array",elements:[{name:"any"}],raw:"any[]"},description:""},onChange:{required:!0,tsType:{name:"signature",type:"function",raw:"(value: any) => void",signature:{arguments:[{type:{name:"any"},name:"value"}],return:{name:"void"}}},description:""},value:{required:!0,tsType:{name:"any"},description:""},renderItem:{required:!0,tsType:{name:"signature",type:"function",raw:"(item: any) => React.ReactNode",signature:{arguments:[{type:{name:"any"},name:"item"}],return:{name:"ReactReactNode",raw:"React.ReactNode"}}},description:""},renderGroup:{required:!0,tsType:{name:"signature",type:"function",raw:"(group: string) => React.ReactNode",signature:{arguments:[{type:{name:"string"},name:"group"}],return:{name:"ReactReactNode",raw:"React.ReactNode"}}},description:""},multiple:{required:!1,tsType:{name:"boolean"},description:""},keyExtractor:{required:!1,tsType:{name:"signature",type:"function",raw:"(item: any) => string",signature:{arguments:[{type:{name:"any"},name:"item"}],return:{name:"string"}}},description:""},showSelectedAtTop:{required:!1,tsType:{name:"boolean"},description:""},defaultOpen:{required:!1,tsType:{name:"boolean"},description:""},children:{required:!0,tsType:{name:"ReactReactNode",raw:"React.ReactNode"},description:""}}};const D=b,z={title:"Example/SelectMenu",component:l,parameters:{layout:"centered"},tags:["autodocs"],argTypes:{items:[1,2,3,4]},args:{onChange:x()}},s=()=>t.createElement(l,{items:M,renderItem:F,onChange:function(a){console.log("Selected items",a)},value:[3,7,9],renderGroup:function(a){throw new Error("Function not implemented.")},multiple:!0,defaultOpen:!0},t.createElement(D,null,"Click Me!!")),F=({fruit:a})=>t.createElement("div",null,`${a.emoji} ${a.name}`);s.__docgenInfo={description:"",methods:[],displayName:"SingleSelect"};var f,S,h;s.parameters={...s.parameters,docs:{...(f=s.parameters)==null?void 0:f.docs,source:{originalSource:`() => {
|
||||
return <Select items={fruits} renderItem={Fruit} onChange={function (value: any): void {
|
||||
console.log("Selected items", value);
|
||||
}} value={[3, 7, 9]} renderGroup={function (group: string): React.ReactNode {
|
||||
throw new Error("Function not implemented.");
|
||||
}} multiple defaultOpen>
|
||||
<SelectButton>Click Me!!</SelectButton>
|
||||
</Select>;
|
||||
}`,...(h=(S=s.parameters)==null?void 0:S.docs)==null?void 0:h.source}}};const H=["SingleSelect"];export{s as SingleSelect,H as __namedExportsOrder,z as default};
|
||||
@@ -0,0 +1 @@
|
||||
var s=Object.create,a=Object.defineProperty,c=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,u=Object.getPrototypeOf,O=Object.prototype.hasOwnProperty,l=(e,r)=>function(){return e&&(r=(0,e[o(e)[0]])(e=0)),r},v=(e,r)=>function(){return r||(0,e[o(e)[0]])((r={exports:{}}).exports,r),r.exports},b=(e,r)=>{for(var t in r)a(e,t,{get:r[t],enumerable:!0})},n=(e,r,t,p)=>{if(r&&typeof r=="object"||typeof r=="function")for(let _ of o(r))!O.call(e,_)&&_!==t&&a(e,_,{get:()=>r[_],enumerable:!(p=c(r,_))||p.enumerable});return e},P=(e,r,t)=>(t=e!=null?s(u(e)):{},n(!e||!e.__esModule?a(t,"default",{value:e,enumerable:!0}):t,e)),y=e=>n(a({},"__esModule",{value:!0}),e);export{b as _,P as a,v as b,l as c,y as d};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./DropdownMenu.stories-BDxDk4nE.js","./index-Cqyox1Tj.js","./fruits-CZbL9byb.js","./index-DtqWk5t9.js","./index-DGXSSr1l.js","./Select.stories-MZU715vD.js","./index-B5ZI-g0m.js","./entry-preview-vYpJuD7B.js","./chunk-XP5HYGXS-BGCqD1aY.js","./entry-preview-docs-DhRFChcp.js","./index-CHGET4sZ.js","./preview-D77C14du.js","./index-DrFu-skq.js","./preview-BWzBA1C2.js","./preview-DtwGhum3.js","./preview-C5MXvx14.js","./preview-BA_NYCcE.css"])))=>i.map(i=>d[i]);
|
||||
import"../sb-preview/runtime.js";(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))u(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const i of r.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&u(i)}).observe(document,{childList:!0,subtree:!0});function l(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function u(e){if(e.ep)return;e.ep=!0;const r=l(e);fetch(e.href,r)}})();const R="modulepreload",T=function(t,n){return new URL(t,n).href},p={},_=function(n,l,u){let e=Promise.resolve();if(l&&l.length>0){const i=document.getElementsByTagName("link"),o=document.querySelector("meta[property=csp-nonce]"),d=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));e=Promise.allSettled(l.map(s=>{if(s=T(s,u),s in p)return;p[s]=!0;const a=s.endsWith(".css"),f=a?'[rel="stylesheet"]':"";if(!!u)for(let m=i.length-1;m>=0;m--){const E=i[m];if(E.href===s&&(!a||E.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${s}"]${f}`))return;const c=document.createElement("link");if(c.rel=a?"stylesheet":R,a||(c.as="script"),c.crossOrigin="",c.href=s,d&&c.setAttribute("nonce",d),document.head.appendChild(c),a)return new Promise((m,E)=>{c.addEventListener("load",m),c.addEventListener("error",()=>E(new Error(`Unable to preload CSS for ${s}`)))})}))}function r(i){const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=i,window.dispatchEvent(o),!o.defaultPrevented)throw i}return e.then(i=>{for(const o of i||[])o.status==="rejected"&&r(o.reason);return n().catch(r)})},{createBrowserChannel:L}=__STORYBOOK_MODULE_CHANNELS__,{addons:S}=__STORYBOOK_MODULE_PREVIEW_API__,O=L({page:"preview"});S.setChannel(O);window.__STORYBOOK_ADDONS_CHANNEL__=O;window.CONFIG_TYPE==="DEVELOPMENT"&&(window.__STORYBOOK_SERVER_CHANNEL__=O);const P={"./src/components/DropdownMenu/DropdownMenu.stories.tsx":async()=>_(()=>import("./DropdownMenu.stories-BDxDk4nE.js"),__vite__mapDeps([0,1,2,3,4]),import.meta.url),"./src/components/Select/Select.stories.tsx":async()=>_(()=>import("./Select.stories-MZU715vD.js"),__vite__mapDeps([5,6,1,2,3,4]),import.meta.url)};async function y(t){return P[t]()}const{composeConfigs:I,PreviewWeb:w,ClientApi:h}=__STORYBOOK_MODULE_PREVIEW_API__,D=async(t=[])=>{const n=await Promise.all([t[0]??_(()=>import("./entry-preview-vYpJuD7B.js"),__vite__mapDeps([7,8,1,4]),import.meta.url),t[1]??_(()=>import("./entry-preview-docs-DhRFChcp.js"),__vite__mapDeps([9,8,10,1]),import.meta.url),t[2]??_(()=>import("./preview-BpGyWwfw.js"),[],import.meta.url),t[3]??_(()=>import("./preview-aVwhiz9X.js"),[],import.meta.url),t[4]??_(()=>import("./preview-D77C14du.js"),__vite__mapDeps([11,12]),import.meta.url),t[5]??_(()=>import("./preview-DFmD0pui.js"),[],import.meta.url),t[6]??_(()=>import("./preview-CFgKly6U.js"),[],import.meta.url),t[7]??_(()=>import("./preview-BWzBA1C2.js"),__vite__mapDeps([13,12]),import.meta.url),t[8]??_(()=>import("./preview-DGUiP6tS.js"),[],import.meta.url),t[9]??_(()=>import("./preview-DtwGhum3.js"),__vite__mapDeps([14,6]),import.meta.url),t[10]??_(()=>import("./preview-C5MXvx14.js"),__vite__mapDeps([15,16]),import.meta.url)]);return I(n)};window.__STORYBOOK_PREVIEW__=window.__STORYBOOK_PREVIEW__||new w(y,D);window.__STORYBOOK_STORY_STORE__=window.__STORYBOOK_STORY_STORE__||window.__STORYBOOK_PREVIEW__.storyStore;export{_};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{R as e}from"./index-Cqyox1Tj.js";const o={},c=e.createContext(o);function u(n){const t=e.useContext(c);return e.useMemo(function(){return typeof n=="function"?n(t):{...t,...n}},[t,n])}function m(n){let t;return n.disableParentContext?t=typeof n.components=="function"?n.components(o):n.components||o:t=u(n.components),e.createElement(c.Provider,{value:t},n.children)}export{m as MDXProvider,u as useMDXComponents};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
||||
function l(o){for(var f=[],i=1;i<arguments.length;i++)f[i-1]=arguments[i];var n=Array.from(typeof o=="string"?[o]:o);n[n.length-1]=n[n.length-1].replace(/\r?\n([\t ]*)$/,"");var s=n.reduce(function(t,g){var a=g.match(/\n([\t ]+|(?!\s).)/g);return a?t.concat(a.map(function(u){var r,e;return(e=(r=u.match(/[\t ]/g))===null||r===void 0?void 0:r.length)!==null&&e!==void 0?e:0})):t},[]);if(s.length){var d=new RegExp(`
|
||||
[ ]{`+Math.min.apply(Math,s)+"}","g");n=n.map(function(t){return t.replace(d,`
|
||||
`)})}n[0]=n[0].replace(/^\r?\n/,"");var c=n[0];return f.forEach(function(t,g){var a=c.match(/(?:^|\n)( *)$/),u=a?a[1]:"",r=t;typeof t=="string"&&t.includes(`
|
||||
`)&&(r=String(t).split(`
|
||||
`).map(function(e,h){return h===0?e:""+u+e}).join(`
|
||||
`)),c+=r+n[g+1]}),c}export{l as d};
|
||||
@@ -0,0 +1,9 @@
|
||||
import{a as v,g as y}from"./index-Cqyox1Tj.js";import{r as E}from"./index-DGXSSr1l.js";var s={exports:{}},t={};/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var f;function O(){if(f)return t;f=1;var _=v(),R=Symbol.for("react.element"),c=Symbol.for("react.fragment"),x=Object.prototype.hasOwnProperty,l=_.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,d={key:!0,ref:!0,__self:!0,__source:!0};function u(o,r,a){var e,n={},i=null,p=null;a!==void 0&&(i=""+a),r.key!==void 0&&(i=""+r.key),r.ref!==void 0&&(p=r.ref);for(e in r)x.call(r,e)&&!d.hasOwnProperty(e)&&(n[e]=r[e]);if(o&&o.defaultProps)for(e in r=o.defaultProps,r)n[e]===void 0&&(n[e]=r[e]);return{$$typeof:R,type:o,key:i,ref:p,props:n,_owner:l.current}}return t.Fragment=c,t.jsx=u,t.jsxs=u,t}var m;function j(){return m||(m=1,s.exports=O()),s.exports}var D=j(),q=E();const F=y(q);export{F,D as j,q as r};
|
||||
@@ -0,0 +1 @@
|
||||
import{m as l,a as c,G as T,K as b,P as d,g as S,L as B,b as L,v as C,w as H,c as u,E as g,$ as y,y as I,d as h,N as k,e as P,D as W,x as A,C as F,f as x,S as D,R,I as f,h as v,i as E,A as M,F as N,j as q,k as w,l as O,W as U,n as Z,T as j,t as z,o as G,p as J,q as K,r as Q,s as V,u as X,z as Y,B as _,H as $,J as aa,M as sa,V as oa,O as ta,Q as ea,U as ra,X as pa,Y as ia,Z as na,_ as ma,a0 as la,a1 as ca,a2 as Ta,a3 as ba,a4 as da,a5 as Sa,a6 as Ba,a7 as La,a8 as Ca,a9 as Ha,aa as ua,ab as ga,ac as ya,ad as Ia}from"./DocsRenderer-CFRXHY34-BAI5cjJR.js";import"./iframe-DUcCSOHa.js";import"../sb-preview/runtime.js";import"./index-Cqyox1Tj.js";import"./index-DtqWk5t9.js";import"./index-DGXSSr1l.js";import"./index-CHGET4sZ.js";import"./index-DrFu-skq.js";import"./react-18-CNJE3xZy.js";export{l as A,c as ActionBar,T as AddonPanel,b as Badge,d as Bar,S as Blockquote,B as Button,L as ClipboardCode,C as Code,H as DL,u as Div,g as DocumentWrapper,y as EmptyTabContent,I as ErrorFormatter,h as FlexBar,k as Form,P as H1,W as H2,A as H3,F as H4,x as H5,D as H6,R as HR,f as IconButton,v as IconButtonSkeleton,E as Icons,M as Img,N as LI,q as Link,w as ListItem,O as Loader,U as Modal,Z as OL,j as P,z as Placeholder,G as Pre,J as ResetWrapper,K as ScrollArea,Q as Separator,V as Spaced,X as Span,Y as StorybookIcon,_ as StorybookLogo,$ as Symbols,aa as SyntaxHighlighter,sa as TT,oa as TabBar,ta as TabButton,ea as TabWrapper,ra as Table,pa as Tabs,ia as TabsState,na as TooltipLinkList,ma as TooltipMessage,la as TooltipNote,ca as UL,Ta as WithTooltip,ba as WithTooltipPure,da as Zoom,Sa as codeCommon,Ba as components,La as createCopyToClipboardFunction,Ca as getStoryHref,Ha as icons,ua as interleaveSeparators,ga as nameSpaceClassNames,ya as resetComponents,Ia as withReset};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,396 @@
|
||||
import{d as $}from"./index-DrFu-skq.js";const{useMemo:x,useEffect:f}=__STORYBOOK_MODULE_PREVIEW_API__,{global:p}=__STORYBOOK_MODULE_GLOBAL__;var m="outline",u=i=>{(Array.isArray(i)?i:[i]).forEach(r)},r=i=>{let t=typeof i=="string"?i:i.join(""),o=p.document.getElementById(t);o&&o.parentElement&&o.parentElement.removeChild(o)},b=(i,t)=>{let o=p.document.getElementById(i);if(o)o.innerHTML!==t&&(o.innerHTML=t);else{let n=p.document.createElement("style");n.setAttribute("id",i),n.innerHTML=t,p.document.head.appendChild(n)}};function s(i){return $`
|
||||
${i} body {
|
||||
outline: 1px solid #2980b9 !important;
|
||||
}
|
||||
|
||||
${i} article {
|
||||
outline: 1px solid #3498db !important;
|
||||
}
|
||||
|
||||
${i} nav {
|
||||
outline: 1px solid #0088c3 !important;
|
||||
}
|
||||
|
||||
${i} aside {
|
||||
outline: 1px solid #33a0ce !important;
|
||||
}
|
||||
|
||||
${i} section {
|
||||
outline: 1px solid #66b8da !important;
|
||||
}
|
||||
|
||||
${i} header {
|
||||
outline: 1px solid #99cfe7 !important;
|
||||
}
|
||||
|
||||
${i} footer {
|
||||
outline: 1px solid #cce7f3 !important;
|
||||
}
|
||||
|
||||
${i} h1 {
|
||||
outline: 1px solid #162544 !important;
|
||||
}
|
||||
|
||||
${i} h2 {
|
||||
outline: 1px solid #314e6e !important;
|
||||
}
|
||||
|
||||
${i} h3 {
|
||||
outline: 1px solid #3e5e85 !important;
|
||||
}
|
||||
|
||||
${i} h4 {
|
||||
outline: 1px solid #449baf !important;
|
||||
}
|
||||
|
||||
${i} h5 {
|
||||
outline: 1px solid #c7d1cb !important;
|
||||
}
|
||||
|
||||
${i} h6 {
|
||||
outline: 1px solid #4371d0 !important;
|
||||
}
|
||||
|
||||
${i} main {
|
||||
outline: 1px solid #2f4f90 !important;
|
||||
}
|
||||
|
||||
${i} address {
|
||||
outline: 1px solid #1a2c51 !important;
|
||||
}
|
||||
|
||||
${i} div {
|
||||
outline: 1px solid #036cdb !important;
|
||||
}
|
||||
|
||||
${i} p {
|
||||
outline: 1px solid #ac050b !important;
|
||||
}
|
||||
|
||||
${i} hr {
|
||||
outline: 1px solid #ff063f !important;
|
||||
}
|
||||
|
||||
${i} pre {
|
||||
outline: 1px solid #850440 !important;
|
||||
}
|
||||
|
||||
${i} blockquote {
|
||||
outline: 1px solid #f1b8e7 !important;
|
||||
}
|
||||
|
||||
${i} ol {
|
||||
outline: 1px solid #ff050c !important;
|
||||
}
|
||||
|
||||
${i} ul {
|
||||
outline: 1px solid #d90416 !important;
|
||||
}
|
||||
|
||||
${i} li {
|
||||
outline: 1px solid #d90416 !important;
|
||||
}
|
||||
|
||||
${i} dl {
|
||||
outline: 1px solid #fd3427 !important;
|
||||
}
|
||||
|
||||
${i} dt {
|
||||
outline: 1px solid #ff0043 !important;
|
||||
}
|
||||
|
||||
${i} dd {
|
||||
outline: 1px solid #e80174 !important;
|
||||
}
|
||||
|
||||
${i} figure {
|
||||
outline: 1px solid #ff00bb !important;
|
||||
}
|
||||
|
||||
${i} figcaption {
|
||||
outline: 1px solid #bf0032 !important;
|
||||
}
|
||||
|
||||
${i} table {
|
||||
outline: 1px solid #00cc99 !important;
|
||||
}
|
||||
|
||||
${i} caption {
|
||||
outline: 1px solid #37ffc4 !important;
|
||||
}
|
||||
|
||||
${i} thead {
|
||||
outline: 1px solid #98daca !important;
|
||||
}
|
||||
|
||||
${i} tbody {
|
||||
outline: 1px solid #64a7a0 !important;
|
||||
}
|
||||
|
||||
${i} tfoot {
|
||||
outline: 1px solid #22746b !important;
|
||||
}
|
||||
|
||||
${i} tr {
|
||||
outline: 1px solid #86c0b2 !important;
|
||||
}
|
||||
|
||||
${i} th {
|
||||
outline: 1px solid #a1e7d6 !important;
|
||||
}
|
||||
|
||||
${i} td {
|
||||
outline: 1px solid #3f5a54 !important;
|
||||
}
|
||||
|
||||
${i} col {
|
||||
outline: 1px solid #6c9a8f !important;
|
||||
}
|
||||
|
||||
${i} colgroup {
|
||||
outline: 1px solid #6c9a9d !important;
|
||||
}
|
||||
|
||||
${i} button {
|
||||
outline: 1px solid #da8301 !important;
|
||||
}
|
||||
|
||||
${i} datalist {
|
||||
outline: 1px solid #c06000 !important;
|
||||
}
|
||||
|
||||
${i} fieldset {
|
||||
outline: 1px solid #d95100 !important;
|
||||
}
|
||||
|
||||
${i} form {
|
||||
outline: 1px solid #d23600 !important;
|
||||
}
|
||||
|
||||
${i} input {
|
||||
outline: 1px solid #fca600 !important;
|
||||
}
|
||||
|
||||
${i} keygen {
|
||||
outline: 1px solid #b31e00 !important;
|
||||
}
|
||||
|
||||
${i} label {
|
||||
outline: 1px solid #ee8900 !important;
|
||||
}
|
||||
|
||||
${i} legend {
|
||||
outline: 1px solid #de6d00 !important;
|
||||
}
|
||||
|
||||
${i} meter {
|
||||
outline: 1px solid #e8630c !important;
|
||||
}
|
||||
|
||||
${i} optgroup {
|
||||
outline: 1px solid #b33600 !important;
|
||||
}
|
||||
|
||||
${i} option {
|
||||
outline: 1px solid #ff8a00 !important;
|
||||
}
|
||||
|
||||
${i} output {
|
||||
outline: 1px solid #ff9619 !important;
|
||||
}
|
||||
|
||||
${i} progress {
|
||||
outline: 1px solid #e57c00 !important;
|
||||
}
|
||||
|
||||
${i} select {
|
||||
outline: 1px solid #e26e0f !important;
|
||||
}
|
||||
|
||||
${i} textarea {
|
||||
outline: 1px solid #cc5400 !important;
|
||||
}
|
||||
|
||||
${i} details {
|
||||
outline: 1px solid #33848f !important;
|
||||
}
|
||||
|
||||
${i} summary {
|
||||
outline: 1px solid #60a1a6 !important;
|
||||
}
|
||||
|
||||
${i} command {
|
||||
outline: 1px solid #438da1 !important;
|
||||
}
|
||||
|
||||
${i} menu {
|
||||
outline: 1px solid #449da6 !important;
|
||||
}
|
||||
|
||||
${i} del {
|
||||
outline: 1px solid #bf0000 !important;
|
||||
}
|
||||
|
||||
${i} ins {
|
||||
outline: 1px solid #400000 !important;
|
||||
}
|
||||
|
||||
${i} img {
|
||||
outline: 1px solid #22746b !important;
|
||||
}
|
||||
|
||||
${i} iframe {
|
||||
outline: 1px solid #64a7a0 !important;
|
||||
}
|
||||
|
||||
${i} embed {
|
||||
outline: 1px solid #98daca !important;
|
||||
}
|
||||
|
||||
${i} object {
|
||||
outline: 1px solid #00cc99 !important;
|
||||
}
|
||||
|
||||
${i} param {
|
||||
outline: 1px solid #37ffc4 !important;
|
||||
}
|
||||
|
||||
${i} video {
|
||||
outline: 1px solid #6ee866 !important;
|
||||
}
|
||||
|
||||
${i} audio {
|
||||
outline: 1px solid #027353 !important;
|
||||
}
|
||||
|
||||
${i} source {
|
||||
outline: 1px solid #012426 !important;
|
||||
}
|
||||
|
||||
${i} canvas {
|
||||
outline: 1px solid #a2f570 !important;
|
||||
}
|
||||
|
||||
${i} track {
|
||||
outline: 1px solid #59a600 !important;
|
||||
}
|
||||
|
||||
${i} map {
|
||||
outline: 1px solid #7be500 !important;
|
||||
}
|
||||
|
||||
${i} area {
|
||||
outline: 1px solid #305900 !important;
|
||||
}
|
||||
|
||||
${i} a {
|
||||
outline: 1px solid #ff62ab !important;
|
||||
}
|
||||
|
||||
${i} em {
|
||||
outline: 1px solid #800b41 !important;
|
||||
}
|
||||
|
||||
${i} strong {
|
||||
outline: 1px solid #ff1583 !important;
|
||||
}
|
||||
|
||||
${i} i {
|
||||
outline: 1px solid #803156 !important;
|
||||
}
|
||||
|
||||
${i} b {
|
||||
outline: 1px solid #cc1169 !important;
|
||||
}
|
||||
|
||||
${i} u {
|
||||
outline: 1px solid #ff0430 !important;
|
||||
}
|
||||
|
||||
${i} s {
|
||||
outline: 1px solid #f805e3 !important;
|
||||
}
|
||||
|
||||
${i} small {
|
||||
outline: 1px solid #d107b2 !important;
|
||||
}
|
||||
|
||||
${i} abbr {
|
||||
outline: 1px solid #4a0263 !important;
|
||||
}
|
||||
|
||||
${i} q {
|
||||
outline: 1px solid #240018 !important;
|
||||
}
|
||||
|
||||
${i} cite {
|
||||
outline: 1px solid #64003c !important;
|
||||
}
|
||||
|
||||
${i} dfn {
|
||||
outline: 1px solid #b4005a !important;
|
||||
}
|
||||
|
||||
${i} sub {
|
||||
outline: 1px solid #dba0c8 !important;
|
||||
}
|
||||
|
||||
${i} sup {
|
||||
outline: 1px solid #cc0256 !important;
|
||||
}
|
||||
|
||||
${i} time {
|
||||
outline: 1px solid #d6606d !important;
|
||||
}
|
||||
|
||||
${i} code {
|
||||
outline: 1px solid #e04251 !important;
|
||||
}
|
||||
|
||||
${i} kbd {
|
||||
outline: 1px solid #5e001f !important;
|
||||
}
|
||||
|
||||
${i} samp {
|
||||
outline: 1px solid #9c0033 !important;
|
||||
}
|
||||
|
||||
${i} var {
|
||||
outline: 1px solid #d90047 !important;
|
||||
}
|
||||
|
||||
${i} mark {
|
||||
outline: 1px solid #ff0053 !important;
|
||||
}
|
||||
|
||||
${i} bdi {
|
||||
outline: 1px solid #bf3668 !important;
|
||||
}
|
||||
|
||||
${i} bdo {
|
||||
outline: 1px solid #6f1400 !important;
|
||||
}
|
||||
|
||||
${i} ruby {
|
||||
outline: 1px solid #ff7b93 !important;
|
||||
}
|
||||
|
||||
${i} rt {
|
||||
outline: 1px solid #ff2f54 !important;
|
||||
}
|
||||
|
||||
${i} rp {
|
||||
outline: 1px solid #803e49 !important;
|
||||
}
|
||||
|
||||
${i} span {
|
||||
outline: 1px solid #cc2643 !important;
|
||||
}
|
||||
|
||||
${i} br {
|
||||
outline: 1px solid #db687d !important;
|
||||
}
|
||||
|
||||
${i} wbr {
|
||||
outline: 1px solid #db175b !important;
|
||||
}`}var e=(i,t)=>{let{globals:o}=t,n=[!0,"true"].includes(o[m]),d=t.viewMode==="docs",l=x(()=>s(d?'[data-story-block="true"]':".sb-show-main"),[t]);return f(()=>{let a=d?`addon-outline-docs-${t.id}`:"addon-outline";return n?b(a,l):u(a),()=>{u(a)}},[n,l,t]),i()},h=[e],g={[m]:!1};export{h as decorators,g as initialGlobals};
|
||||
@@ -0,0 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./DocsRenderer-CFRXHY34-BAI5cjJR.js","./iframe-DUcCSOHa.js","./index-Cqyox1Tj.js","./index-DtqWk5t9.js","./index-DGXSSr1l.js","./index-CHGET4sZ.js","./index-DrFu-skq.js","./react-18-CNJE3xZy.js"])))=>i.map(i=>d[i]);
|
||||
import{_ as a}from"./iframe-DUcCSOHa.js";import"../sb-preview/runtime.js";var i=Object.entries(globalThis.TAGS_OPTIONS??{}).reduce((e,r)=>{let[t,o]=r;return o.excludeFromDocsStories&&(e[t]=!0),e},{}),d={docs:{renderer:async()=>{let{DocsRenderer:e}=await a(()=>import("./DocsRenderer-CFRXHY34-BAI5cjJR.js").then(r=>r.ak),__vite__mapDeps([0,1,2,3,4,5,6,7]),import.meta.url);return new e},stories:{filter:e=>{var r;return(e.tags||[]).filter(t=>i[t]).length===0&&!((r=e.parameters.docs)!=null&&r.disable)}}}};export{d as parameters};
|
||||
@@ -0,0 +1 @@
|
||||
const e={parameters:{controls:{matchers:{color:/(background|color)$/i,date:/Date$/i}}}};export{e as default};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
import{d as Y}from"./index-DrFu-skq.js";const{useEffect:_,useMemo:h}=__STORYBOOK_MODULE_PREVIEW_API__,{global:H}=__STORYBOOK_MODULE_GLOBAL__,{logger:K}=__STORYBOOK_MODULE_CLIENT_LOGGER__;var g="backgrounds",C={light:{name:"light",value:"#F8F8F8"},dark:{name:"dark",value:"#333"}},{document:$,window:T}=H,I=()=>{var r;return!!((r=T==null?void 0:T.matchMedia("(prefers-reduced-motion: reduce)"))!=null&&r.matches)},A=r=>{(Array.isArray(r)?r:[r]).forEach(P)},P=r=>{var t;let e=$.getElementById(r);e&&((t=e.parentElement)==null||t.removeChild(e))},z=(r,e)=>{let t=$.getElementById(r);if(t)t.innerHTML!==e&&(t.innerHTML=e);else{let d=$.createElement("style");d.setAttribute("id",r),d.innerHTML=e,$.head.appendChild(d)}},U=(r,e,t)=>{var a;let d=$.getElementById(r);if(d)d.innerHTML!==e&&(d.innerHTML=e);else{let o=$.createElement("style");o.setAttribute("id",r),o.innerHTML=e;let i=`addon-backgrounds-grid${t?`-docs-${t}`:""}`,n=$.getElementById(i);n?(a=n.parentElement)==null||a.insertBefore(o,n):$.head.appendChild(o)}},j={cellSize:100,cellAmount:10,opacity:.8},w="addon-backgrounds",R="addon-backgrounds-grid",X=I()?"":"transition: background-color 0.3s;",N=(r,e)=>{let{globals:t,parameters:d,viewMode:a,id:o}=e,{options:i=C,disable:n,grid:s=j}=d[g]||{},c=t[g]||{},u=c.value,l=u?i[u]:void 0,b=(l==null?void 0:l.value)||"transparent",f=c.grid||!1,y=!!l&&!n,m=a==="docs"?`#anchor--${o} .docs-story`:".sb-show-main",E=a==="docs"?`#anchor--${o} .docs-story`:".sb-show-main",D=d.layout===void 0||d.layout==="padded",L=a==="docs"?20:D?16:0,{cellAmount:k,cellSize:p,opacity:x,offsetX:v=L,offsetY:S=L}=s,B=a==="docs"?`${w}-docs-${o}`:`${w}-color`,G=a==="docs"?o:null;_(()=>{let O=`
|
||||
${m} {
|
||||
background: ${b} !important;
|
||||
${X}
|
||||
}`;if(!y){A(B);return}U(B,O,G)},[m,B,G,y,b]);let M=a==="docs"?`${R}-docs-${o}`:`${R}`;return _(()=>{if(!f){A(M);return}let O=[`${p*k}px ${p*k}px`,`${p*k}px ${p*k}px`,`${p}px ${p}px`,`${p}px ${p}px`].join(", "),F=`
|
||||
${E} {
|
||||
background-size: ${O} !important;
|
||||
background-position: ${v}px ${S}px, ${v}px ${S}px, ${v}px ${S}px, ${v}px ${S}px !important;
|
||||
background-blend-mode: difference !important;
|
||||
background-image: linear-gradient(rgba(130, 130, 130, ${x}) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(130, 130, 130, ${x}) 1px, transparent 1px),
|
||||
linear-gradient(rgba(130, 130, 130, ${x/2}) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(130, 130, 130, ${x/2}) 1px, transparent 1px) !important;
|
||||
}
|
||||
`;z(M,F)},[k,p,E,M,f,v,S,x]),r()},W=(r,e=[],t)=>{if(r==="transparent")return"transparent";if(e.find(a=>a.value===r)||r)return r;let d=e.find(a=>a.name===t);if(d)return d.value;if(t){let a=e.map(o=>o.name).join(", ");K.warn(Y`
|
||||
Backgrounds Addon: could not find the default color "${t}".
|
||||
These are the available colors for your story based on your configuration:
|
||||
${a}.
|
||||
`)}return"transparent"},q=(r,e)=>{var u;let{globals:t,parameters:d}=e,a=(u=t[g])==null?void 0:u.value,o=d[g],i=h(()=>o.disable?"transparent":W(a,o.values,o.default),[o,a]),n=h(()=>i&&i!=="transparent",[i]),s=e.viewMode==="docs"?`#anchor--${e.id} .docs-story`:".sb-show-main",c=h(()=>`
|
||||
${s} {
|
||||
background: ${i} !important;
|
||||
${I()?"":"transition: background-color 0.3s;"}
|
||||
}
|
||||
`,[i,s]);return _(()=>{let l=e.viewMode==="docs"?`addon-backgrounds-docs-${e.id}`:"addon-backgrounds-color";if(!n){A(l);return}U(l,c,e.viewMode==="docs"?e.id:null)},[n,c,e]),r()},J=(r,e)=>{var y;let{globals:t,parameters:d}=e,a=d[g].grid,o=((y=t[g])==null?void 0:y.grid)===!0&&a.disable!==!0,{cellAmount:i,cellSize:n,opacity:s}=a,c=e.viewMode==="docs",u=d.layout===void 0||d.layout==="padded"?16:0,l=a.offsetX??(c?20:u),b=a.offsetY??(c?20:u),f=h(()=>{let m=e.viewMode==="docs"?`#anchor--${e.id} .docs-story`:".sb-show-main",E=[`${n*i}px ${n*i}px`,`${n*i}px ${n*i}px`,`${n}px ${n}px`,`${n}px ${n}px`].join(", ");return`
|
||||
${m} {
|
||||
background-size: ${E} !important;
|
||||
background-position: ${l}px ${b}px, ${l}px ${b}px, ${l}px ${b}px, ${l}px ${b}px !important;
|
||||
background-blend-mode: difference !important;
|
||||
background-image: linear-gradient(rgba(130, 130, 130, ${s}) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(130, 130, 130, ${s}) 1px, transparent 1px),
|
||||
linear-gradient(rgba(130, 130, 130, ${s/2}) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(130, 130, 130, ${s/2}) 1px, transparent 1px) !important;
|
||||
}
|
||||
`},[n]);return _(()=>{let m=e.viewMode==="docs"?`addon-backgrounds-grid-docs-${e.id}`:"addon-backgrounds-grid";if(!o){A(m);return}z(m,f)},[o,f,e]),r()},V=FEATURES!=null&&FEATURES.backgroundsStoryGlobals?[N]:[J,q],ee={[g]:{grid:{cellSize:20,opacity:.5,cellAmount:5},disable:!1,...!(FEATURES!=null&&FEATURES.backgroundsStoryGlobals)&&{values:Object.values(C)}}},Q={[g]:{value:void 0,grid:!1}},re=FEATURES!=null&&FEATURES.backgroundsStoryGlobals?Q:{[g]:null};export{V as decorators,re as initialGlobals,ee as parameters};
|
||||
@@ -0,0 +1 @@
|
||||
var e="viewport",o={[e]:{value:void 0,isRotated:!1}},t={viewport:"reset",viewportRotated:!1},r=FEATURES!=null&&FEATURES.viewportStoryGlobals?o:t;export{r as initialGlobals};
|
||||
@@ -0,0 +1,7 @@
|
||||
const{STORY_CHANGED:r}=__STORYBOOK_MODULE_CORE_EVENTS__,{addons:s}=__STORYBOOK_MODULE_PREVIEW_API__,{global:O}=__STORYBOOK_MODULE_GLOBAL__;var d="storybook/highlight",i="storybookHighlight",g=`${d}/add`,E=`${d}/reset`,{document:l}=O,H=(e="#FF4785",t="dashed")=>`
|
||||
outline: 2px ${t} ${e};
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 6px rgba(255,255,255,0.6);
|
||||
`,h=s.getChannel(),T=e=>{let t=i;n();let o=Array.from(new Set(e.elements)),_=l.createElement("style");_.setAttribute("id",t),_.innerHTML=o.map(a=>`${a}{
|
||||
${H(e.color,e.style)}
|
||||
}`).join(" "),l.head.appendChild(_)},n=()=>{var o;let e=i,t=l.getElementById(e);t&&((o=t.parentNode)==null||o.removeChild(t))};h.on(r,n);h.on(E,n);h.on(g,T);
|
||||
@@ -0,0 +1 @@
|
||||
import{i as r}from"./index-B5ZI-g0m.js";var{step:a}=r({step:(p,t,e)=>t(e)},{intercept:!0}),i={throwPlayFunctionExceptions:!1};export{i as parameters,a as runStep};
|
||||
@@ -0,0 +1 @@
|
||||
let l;const S=new Uint8Array(16);function A(){if(!l&&(l=typeof crypto<"u"&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto),!l))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return l(S)}const o=[];for(let e=0;e<256;++e)o.push((e+256).toString(16).slice(1));function D(e,t=0){return o[e[t+0]]+o[e[t+1]]+o[e[t+2]]+o[e[t+3]]+"-"+o[e[t+4]]+o[e[t+5]]+"-"+o[e[t+6]]+o[e[t+7]]+"-"+o[e[t+8]]+o[e[t+9]]+"-"+o[e[t+10]]+o[e[t+11]]+o[e[t+12]]+o[e[t+13]]+o[e[t+14]]+o[e[t+15]]}const I=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),m={randomUUID:I};function v(e,t,r){if(m.randomUUID&&!t&&!e)return m.randomUUID();e=e||{};const n=e.random||(e.rng||A)();return n[6]=n[6]&15|64,n[8]=n[8]&63|128,D(n)}const{addons:w}=__STORYBOOK_MODULE_PREVIEW_API__,{ImplicitActionsDuringRendering:U}=__STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__,{global:p}=__STORYBOOK_MODULE_GLOBAL__;var f="storybook/actions",j=`${f}/action-event`,V={depth:10,clearOnStoryChange:!0,limit:50},E=(e,t)=>{let r=Object.getPrototypeOf(e);return!r||t(r)?r:E(r,t)},C=e=>!!(typeof e=="object"&&e&&E(e,t=>/^Synthetic(?:Base)?Event$/.test(t.constructor.name))&&typeof e.persist=="function"),K=e=>{if(C(e)){let t=Object.create(e.constructor.prototype,Object.getOwnPropertyDescriptors(e));t.persist();let r=Object.getOwnPropertyDescriptor(t,"view"),n=r==null?void 0:r.value;return typeof n=="object"&&(n==null?void 0:n.constructor.name)==="Window"&&Object.defineProperty(t,"view",{...r,value:Object.create(n.constructor.prototype)}),t}return e},L=()=>typeof crypto=="object"&&typeof crypto.getRandomValues=="function"?v():Date.now().toString(36)+Math.random().toString(36).substring(2);function u(e,t={}){let r={...V,...t},n=function(...c){var g,O;if(t.implicit){let y=(g="__STORYBOOK_PREVIEW__"in p?p.__STORYBOOK_PREVIEW__:void 0)==null?void 0:g.storyRenders.find(d=>d.phase==="playing"||d.phase==="rendering");if(y){let d=!((O=window==null?void 0:window.FEATURES)!=null&&O.disallowImplicitActionsInRenderV8),R=new U({phase:y.phase,name:e,deprecated:d});if(d)console.warn(R);else throw R}}let i=w.getChannel(),s=L(),a=5,_=c.map(K),b=c.length>1?_:_[0],x={id:s,count:0,data:{name:e,args:b},options:{...r,maxDepth:a+(r.depth||3),allowFunction:r.allowFunction||!1}};i.emit(j,x)};return n.isAction=!0,n.implicit=t.implicit,n}var T=(e,t)=>typeof t[e]>"u"&&!(e in t),B=e=>{let{initialArgs:t,argTypes:r,id:n,parameters:{actions:c}}=e;if(!c||c.disable||!c.argTypesRegex||!r)return{};let i=new RegExp(c.argTypesRegex);return Object.entries(r).filter(([s])=>!!i.test(s)).reduce((s,[a,_])=>(T(a,t)&&(s[a]=u(a,{implicit:!0,id:n})),s),{})},M=e=>{let{initialArgs:t,argTypes:r,parameters:{actions:n}}=e;return n!=null&&n.disable||!r?{}:Object.entries(r).filter(([c,i])=>!!i.action).reduce((c,[i,s])=>(T(i,t)&&(c[i]=u(typeof s.action=="string"?s.action:i)),c),{})},Y=[M,B],h=!1,P=e=>{let{parameters:{actions:t}}=e;if(!(t!=null&&t.disable)&&!h&&"__STORYBOOK_TEST_ON_MOCK_CALL__"in p&&typeof p.__STORYBOOK_TEST_ON_MOCK_CALL__=="function"){let r=p.__STORYBOOK_TEST_ON_MOCK_CALL__;r((n,c)=>{let i=n.getMockName();i!=="spy"&&(!/^next\/.*::/.test(i)||["next/router::useRouter()","next/navigation::useRouter()","next/navigation::redirect","next/cache::","next/headers::cookies().set","next/headers::cookies().delete","next/headers::headers().set","next/headers::headers().delete"].some(s=>i.startsWith(s)))&&u(i)(c)}),h=!0}},N=[P];export{Y as argsEnhancers,N as loaders};
|
||||
@@ -0,0 +1 @@
|
||||
import{r as i}from"./index-Cqyox1Tj.js";import{r as m}from"./index-DGXSSr1l.js";var n={},u;function c(){if(u)return n;u=1;var e=m();return n.createRoot=e.createRoot,n.hydrateRoot=e.hydrateRoot,n}var R=c(),s=new Map;function v(){return globalThis.IS_REACT_ACT_ENVIRONMENT}var f=({callback:e,children:t})=>{let r=i.useRef();return i.useLayoutEffect(()=>{r.current!==e&&(r.current=e,e())},[e]),t};typeof Promise.withResolvers>"u"&&(Promise.withResolvers=()=>{let e=null,t=null;return{promise:new Promise((r,o)=>{e=r,t=o}),resolve:e,reject:t}});var d=async(e,t,r)=>{let o=await p(t,r);if(v()){o.render(e);return}let{promise:a,resolve:l}=Promise.withResolvers();return o.render(i.createElement(f,{callback:l},e)),a},w=(e,t)=>{let r=s.get(e);r&&(r.unmount(),s.delete(e))},p=async(e,t)=>{let r=s.get(e);return r||(r=R.createRoot(e,t),s.set(e,r)),r};export{d as renderElement,w as unmountElement};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svgjs="http://svgjs.com/svgjs" xmlns:xlink="http://www.w3.org/1999/xlink" width="164" height="164" version="1.1"><svg xmlns="http://www.w3.org/2000/svg" width="164" height="164" fill="none" viewBox="0 0 164 164"><path fill="#FF4785" d="M22.467 147.762 17.5 15.402a8.062 8.062 0 0 1 7.553-8.35L137.637.016a8.061 8.061 0 0 1 8.565 8.047v144.23a8.063 8.063 0 0 1-8.424 8.054l-107.615-4.833a8.062 8.062 0 0 1-7.695-7.752Z"/><path fill="#fff" fill-rule="evenodd" d="m128.785.57-15.495.968-.755 18.172a1.203 1.203 0 0 0 1.928 1.008l7.06-5.354 5.962 4.697a1.202 1.202 0 0 0 1.946-.987L128.785.569Zm-12.059 60.856c-2.836 2.203-23.965 3.707-23.965.57.447-11.969-4.912-12.494-7.889-12.494-2.828 0-7.59.855-7.59 7.267 0 6.534 6.96 10.223 15.13 14.553 11.607 6.15 25.654 13.594 25.654 32.326 0 17.953-14.588 27.871-33.194 27.871-19.201 0-35.981-7.769-34.086-34.702.744-3.163 25.156-2.411 25.156 0-.298 11.114 2.232 14.383 8.633 14.383 4.912 0 7.144-2.708 7.144-7.267 0-6.9-7.252-10.973-15.595-15.657C64.827 81.933 51.53 74.468 51.53 57.34c0-17.098 11.76-28.497 32.747-28.497 20.988 0 32.449 11.224 32.449 32.584Z" clip-rule="evenodd"/></svg><style>@media (prefers-color-scheme:light){:root{filter:none}}</style></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,640 @@
|
||||
<!doctype html>
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Storybook</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Nunito Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./sb-common-assets/nunito-sans-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./sb-common-assets/nunito-sans-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('./sb-common-assets/nunito-sans-bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('./sb-common-assets/nunito-sans-bold-italic.woff2') format('woff2');
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.CONFIG_TYPE = 'PRODUCTION';
|
||||
window.LOGLEVEL = 'info';
|
||||
window.FRAMEWORK_OPTIONS = {};
|
||||
window.CHANNEL_OPTIONS = {};
|
||||
window.FEATURES = {"argTypeTargetsV7":true,"legacyDecoratorFileOrder":false,"disallowImplicitActionsInRenderV8":true};
|
||||
window.STORIES = [{"titlePrefix":"","directory":"./src","files":"**/*.mdx","importPathMatcher":"^\\.[\\\\/](?:src(?:\\/(?!\\.)(?:(?:(?!(?:^|\\/)\\.).)*?)\\/|\\/|$)(?!\\.)(?=.)[^/]*?\\.mdx)$"},{"titlePrefix":"","directory":"./src","files":"**/*.stories.@(js|jsx|mjs|ts|tsx)","importPathMatcher":"^\\.[\\\\/](?:src(?:\\/(?!\\.)(?:(?:(?!(?:^|\\/)\\.).)*?)\\/|\\/|$)(?!\\.)(?=.)[^/]*?\\.stories\\.(js|jsx|mjs|ts|tsx))$"}];
|
||||
window.DOCS_OPTIONS = {"defaultName":"Docs","autodocs":"tag"};
|
||||
window.TAGS_OPTIONS = {"dev-only":{"excludeFromDocsStories":true},"docs-only":{"excludeFromSidebar":true},"test-only":{"excludeFromSidebar":true,"excludeFromDocsStories":true}};
|
||||
|
||||
|
||||
|
||||
// We do this so that "module && module.hot" etc. in Storybook source code
|
||||
// doesn't fail (it will simply be disabled)
|
||||
window.module = undefined;
|
||||
window.global = window;
|
||||
</script>
|
||||
<base target="_parent" />
|
||||
|
||||
<style>
|
||||
/* While we aren't showing the main block yet, but still preparing, we want everything the user
|
||||
has rendered, which may or may not be in #storybook-root, to be display none */
|
||||
.sb-show-preparing-story:not(.sb-show-main) > :not(.sb-preparing-story) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sb-show-preparing-docs:not(.sb-show-main) > :not(.sb-preparing-docs) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide our own blocks when we aren't supposed to be showing them */
|
||||
:not(.sb-show-preparing-story) > .sb-preparing-story,
|
||||
:not(.sb-show-preparing-docs) > .sb-preparing-docs,
|
||||
:not(.sb-show-nopreview) > .sb-nopreview,
|
||||
:not(.sb-show-errordisplay) > .sb-errordisplay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sb-show-main.sb-main-centered {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sb-show-main.sb-main-centered #storybook-root {
|
||||
box-sizing: border-box;
|
||||
margin: auto;
|
||||
padding: 1rem;
|
||||
max-height: 100%;
|
||||
/* Hack for centering correctly in IE11 */
|
||||
}
|
||||
|
||||
/* Vertical centering fix for IE11 */
|
||||
@media screen and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
.sb-show-main.sb-main-centered:after {
|
||||
content: '';
|
||||
min-height: inherit;
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sb-show-main.sb-main-fullscreen {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sb-show-main.sb-main-padded {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sb-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 40px;
|
||||
font-family:
|
||||
'Nunito Sans',
|
||||
-apple-system,
|
||||
'.SFNSText-Regular',
|
||||
'San Francisco',
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
'Helvetica Neue',
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.sb-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.sb-wrapper {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sb-heading {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
margin: 10px 0;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.sb-nopreview {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sb-nopreview_main {
|
||||
margin: auto;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.sb-nopreview_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sb-errordisplay {
|
||||
background: #f6f9fc;
|
||||
color: black;
|
||||
z-index: 999999;
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
& ol {
|
||||
padding-left: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
font-family: Nunito Sans;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 30px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #ff4400;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
& p,
|
||||
& ol {
|
||||
font-family: Nunito Sans;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 19px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& li + li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
& a {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.sb-errordisplay_main {
|
||||
margin: auto;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
border: 1px solid #ff0000;
|
||||
box-shadow: 0 0 64px rgba(0, 0, 0, 0.1);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.sb-errordisplay_code {
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
background: #242424;
|
||||
color: #c6c6c6;
|
||||
box-sizing: border-box;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 19px;
|
||||
border-radius: 4px;
|
||||
|
||||
font-family: 'Operator Mono', 'Fira Code Retina', 'Fira Code', 'FiraCode-Retina', 'Andale Mono',
|
||||
'Lucida Console', Consolas, Monaco, monospace;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sb-errordisplay pre {
|
||||
white-space: pre-wrap;
|
||||
white-space: revert;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sb-rotate360 {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sb-rotate360 {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes sb-glow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sb-glow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* We display the preparing loaders *over* the rendering story */
|
||||
.sb-preparing-story,
|
||||
.sb-preparing-docs {
|
||||
background-color: white;
|
||||
/* Maximum possible z-index. It would be better to use stacking contexts to ensure it's always
|
||||
on top, but this isn't possible as it would require making CSS changes that could affect user code */
|
||||
z-index: 2147483647;
|
||||
}
|
||||
|
||||
.sb-loader {
|
||||
-webkit-animation: sb-rotate360 0.7s linear infinite;
|
||||
animation: sb-rotate360 0.7s linear infinite;
|
||||
border-color: rgba(97, 97, 97, 0.29);
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
border-top-color: #646464;
|
||||
border-width: 2px;
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
left: 50%;
|
||||
margin-left: -16px;
|
||||
margin-top: -16px;
|
||||
mix-blend-mode: difference;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transition: all 200ms ease-out;
|
||||
vertical-align: top;
|
||||
width: 32px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.sb-previewBlock {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 3px 0;
|
||||
margin: 25px auto 40px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.sb-previewBlock_header {
|
||||
align-items: center;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0 -1px 0 0 inset;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.sb-previewBlock_icon {
|
||||
-webkit-animation: sb-glow 1.5s ease-in-out infinite;
|
||||
animation: sb-glow 1.5s ease-in-out infinite;
|
||||
background: #e6e6e6;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.sb-previewBlock_icon:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sb-previewBlock_body {
|
||||
-webkit-animation: sb-glow 1.5s ease-in-out infinite;
|
||||
animation: sb-glow 1.5s ease-in-out infinite;
|
||||
height: 182px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sb-argstableBlock {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin: 25px auto 40px;
|
||||
max-width: 600px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sb-argstableBlock th:first-of-type,
|
||||
.sb-argstableBlock td:first-of-type {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock th:nth-of-type(2),
|
||||
.sb-argstableBlock td:nth-of-type(2) {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.sb-argstableBlock th:nth-of-type(3),
|
||||
.sb-argstableBlock td:nth-of-type(3) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.sb-argstableBlock th:last-of-type,
|
||||
.sb-argstableBlock td:last-of-type {
|
||||
width: 25%;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock th span,
|
||||
.sb-argstableBlock td span {
|
||||
-webkit-animation: sb-glow 1.5s ease-in-out infinite;
|
||||
animation: sb-glow 1.5s ease-in-out infinite;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.sb-argstableBlock th {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body {
|
||||
border-radius: 4px;
|
||||
box-shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 1px 3px 1px,
|
||||
rgba(0, 0, 0, 0.065) 0 0 0 1px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body tr {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body tr:not(:first-child) {
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body tr:first-child td:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body tr:first-child td:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body tr:last-child td:first-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body tr:last-child td:last-child {
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body td {
|
||||
background: #fff;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body td:not(:first-of-type) {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-body button {
|
||||
-webkit-animation: sb-glow 1.5s ease-in-out infinite;
|
||||
animation: sb-glow 1.5s ease-in-out infinite;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
color: transparent;
|
||||
display: inline;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-summary {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sb-argstableBlock-code {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* globals window */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
try {
|
||||
if (window.top !== window) {
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.top.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
window.__VUE_DEVTOOLS_GLOBAL_HOOK__ = window.top.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
||||
window.top.__VUE_DEVTOOLS_CONTEXT__ = window.document;
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('unable to connect to top frame for connecting dev tools');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin src="./assets/iframe-DUcCSOHa.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="portal-root"></div>
|
||||
<div class="sb-preparing-story sb-wrapper">
|
||||
<div class="sb-loader"></div>
|
||||
</div>
|
||||
|
||||
<div class="sb-preparing-docs sb-wrapper">
|
||||
<div class="sb-previewBlock">
|
||||
<div class="sb-previewBlock_header">
|
||||
<div class="sb-previewBlock_icon"></div>
|
||||
<div class="sb-previewBlock_icon"></div>
|
||||
<div class="sb-previewBlock_icon"></div>
|
||||
<div class="sb-previewBlock_icon"></div>
|
||||
</div>
|
||||
<div class="sb-previewBlock_body">
|
||||
<div class="sb-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<table aria-hidden="true" class="sb-argstableBlock">
|
||||
<thead class="sb-argstableBlock-head">
|
||||
<tr>
|
||||
<th><span>Name</span></th>
|
||||
<th><span>Description</span></th>
|
||||
<th><span>Default</span></th>
|
||||
<th><span>Control </span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sb-argstableBlock-body">
|
||||
<tr>
|
||||
<td><span>propertyName</span><span title="Required">*</span></td>
|
||||
<td>
|
||||
<div><span>This is a short description</span></div>
|
||||
<div class="sb-argstableBlock-summary">
|
||||
<div><span class="sb-argstableBlock-code">summary</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div><span class="sb-argstableBlock-code">defaultValue</span></div>
|
||||
</td>
|
||||
<td><button>Set string</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span>propertyName</span><span>*</span></td>
|
||||
<td>
|
||||
<div><span>This is a short description</span></div>
|
||||
<div class="sb-argstableBlock-summary">
|
||||
<div><span class="sb-argstableBlock-code">summary</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div><span class="sb-argstableBlock-code">defaultValue</span></div>
|
||||
</td>
|
||||
<td><button>Set string</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span>propertyName</span><span>*</span></td>
|
||||
<td>
|
||||
<div><span>This is a short description</span></div>
|
||||
<div class="sb-argstableBlock-summary">
|
||||
<div><span class="sb-argstableBlock-code">summary</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div><span class="sb-argstableBlock-code">defaultValue</span></div>
|
||||
</td>
|
||||
<td><button>Set string</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="sb-nopreview sb-wrapper">
|
||||
<div class="sb-nopreview_main">
|
||||
<h1 class="sb-nopreview_heading sb-heading">No Preview</h1>
|
||||
<p>Sorry, but you either have no stories or none are selected somehow.</p>
|
||||
<ul>
|
||||
<li>Please check the Storybook config.</li>
|
||||
<li>Try reloading the page.</li>
|
||||
</ul>
|
||||
<p>
|
||||
If the problem persists, check the browser console, or the terminal you've run Storybook from.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sb-errordisplay sb-wrapper">
|
||||
<div class="sb-errordisplay_main">
|
||||
<h1 id="error-message"></h1>
|
||||
<p>
|
||||
The component failed to render properly, likely due to a configuration issue in Storybook.
|
||||
Here are some common causes and how you can address them:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Missing Context/Providers</strong>: You can use decorators to supply specific
|
||||
contexts or providers, which are sometimes necessary for components to render correctly. For
|
||||
detailed instructions on using decorators, please visit the
|
||||
<a href="https://storybook.js.org/docs/writing-stories/decorators"
|
||||
>Decorators documentation</a
|
||||
>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Misconfigured Webpack or Vite</strong>: Verify that Storybook picks up all necessary
|
||||
settings for loaders, plugins, and other relevant parameters. You can find step-by-step
|
||||
guides for configuring
|
||||
<a href="https://storybook.js.org/docs/builders/webpack">Webpack</a> or
|
||||
<a href="https://storybook.js.org/docs/builders/vite">Vite</a>
|
||||
with Storybook.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Missing Environment Variables</strong>: Your Storybook may require specific
|
||||
environment variables to function as intended. You can set up custom environment variables
|
||||
as outlined in the
|
||||
<a href="https://storybook.js.org/docs/configure/environment-variables"
|
||||
>Environment Variables documentation</a
|
||||
>.
|
||||
</li>
|
||||
</ol>
|
||||
<pre class="sb-errordisplay_code"><code id="error-stack"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="storybook-root"></div>
|
||||
<div id="storybook-docs"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>@storybook/core - Storybook</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Nunito Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./sb-common-assets/nunito-sans-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./sb-common-assets/nunito-sans-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('./sb-common-assets/nunito-sans-bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('./sb-common-assets/nunito-sans-bold-italic.woff2') format('woff2');
|
||||
}
|
||||
</style>
|
||||
|
||||
<link href="./sb-manager/runtime.js" rel="modulepreload" />
|
||||
|
||||
|
||||
<link href="./sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/onboarding-1/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/essentials-controls-2/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/essentials-actions-3/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/essentials-backgrounds-4/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/essentials-viewport-5/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/essentials-toolbars-6/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/essentials-measure-7/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/essentials-outline-8/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/chromatic-com-storybook-9/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
<link href="./sb-addons/interactions-10/manager-bundle.js" rel="modulepreload" />
|
||||
|
||||
|
||||
<style>
|
||||
#storybook-root[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
window['FEATURES'] = {
|
||||
"argTypeTargetsV7": true,
|
||||
"legacyDecoratorFileOrder": false,
|
||||
"disallowImplicitActionsInRenderV8": true
|
||||
};
|
||||
|
||||
|
||||
|
||||
window['REFS'] = {};
|
||||
|
||||
|
||||
|
||||
window['LOGLEVEL'] = "info";
|
||||
|
||||
|
||||
|
||||
window['DOCS_OPTIONS'] = {
|
||||
"defaultName": "Docs",
|
||||
"autodocs": "tag"
|
||||
};
|
||||
|
||||
|
||||
|
||||
window['CONFIG_TYPE'] = "PRODUCTION";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
window['TAGS_OPTIONS'] = {
|
||||
"dev-only": {
|
||||
"excludeFromDocsStories": true
|
||||
},
|
||||
"docs-only": {
|
||||
"excludeFromSidebar": true
|
||||
},
|
||||
"test-only": {
|
||||
"excludeFromSidebar": true,
|
||||
"excludeFromDocsStories": true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
window['STORYBOOK_RENDERER'] = "react";
|
||||
|
||||
|
||||
|
||||
window['STORYBOOK_BUILDER'] = "@storybook/builder-vite";
|
||||
|
||||
|
||||
|
||||
window['STORYBOOK_FRAMEWORK'] = "/Users/satish/work/plane-ce/node_modules/@storybook/react-vite";
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<script type="module">
|
||||
import './sb-manager/globals-runtime.js';
|
||||
|
||||
|
||||
import './sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js';
|
||||
|
||||
import './sb-addons/onboarding-1/manager-bundle.js';
|
||||
|
||||
import './sb-addons/essentials-controls-2/manager-bundle.js';
|
||||
|
||||
import './sb-addons/essentials-actions-3/manager-bundle.js';
|
||||
|
||||
import './sb-addons/essentials-backgrounds-4/manager-bundle.js';
|
||||
|
||||
import './sb-addons/essentials-viewport-5/manager-bundle.js';
|
||||
|
||||
import './sb-addons/essentials-toolbars-6/manager-bundle.js';
|
||||
|
||||
import './sb-addons/essentials-measure-7/manager-bundle.js';
|
||||
|
||||
import './sb-addons/essentials-outline-8/manager-bundle.js';
|
||||
|
||||
import './sb-addons/chromatic-com-storybook-9/manager-bundle.js';
|
||||
|
||||
import './sb-addons/interactions-10/manager-bundle.js';
|
||||
|
||||
|
||||
import './sb-manager/runtime.js';
|
||||
</script>
|
||||
|
||||
|
||||
<link href="./sb-preview/runtime.js" rel="prefetch" as="script" />
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{"v":5,"entries":{"example-dropdownmenu--docs":{"id":"example-dropdownmenu--docs","title":"Example/DropdownMenu","name":"Docs","importPath":"./src/components/DropdownMenu/DropdownMenu.stories.tsx","type":"docs","tags":["dev","test","autodocs"],"storiesImports":[]},"example-dropdownmenu--default-dropdown":{"type":"story","id":"example-dropdownmenu--default-dropdown","name":"Default Dropdown","title":"Example/DropdownMenu","importPath":"./src/components/DropdownMenu/DropdownMenu.stories.tsx","componentPath":"./src/components/DropdownMenu/DropdownMenu.tsx","tags":["dev","test","autodocs"]},"example-dropdownmenu--multi-select":{"type":"story","id":"example-dropdownmenu--multi-select","name":"Multi Select","title":"Example/DropdownMenu","importPath":"./src/components/DropdownMenu/DropdownMenu.stories.tsx","componentPath":"./src/components/DropdownMenu/DropdownMenu.tsx","tags":["dev","test","autodocs"]},"example-dropdownmenu--nested-dropdown":{"type":"story","id":"example-dropdownmenu--nested-dropdown","name":"Nested Dropdown","title":"Example/DropdownMenu","importPath":"./src/components/DropdownMenu/DropdownMenu.stories.tsx","componentPath":"./src/components/DropdownMenu/DropdownMenu.tsx","tags":["dev","test","autodocs"]},"example-selectmenu--docs":{"id":"example-selectmenu--docs","title":"Example/SelectMenu","name":"Docs","importPath":"./src/components/Select/Select.stories.tsx","type":"docs","tags":["dev","test","autodocs"],"storiesImports":[]},"example-selectmenu--single-select":{"type":"story","id":"example-selectmenu--single-select","name":"Single Select","title":"Example/SelectMenu","importPath":"./src/components/Select/Select.stories.tsx","componentPath":"./src/components/Select/Select.tsx","tags":["dev","test","autodocs"]}}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user