Compare commits

..

12 Commits

Author SHA1 Message Date
Satish Gandham 039a8233a1 Optimize select component and cleanup story book 2024-12-30 14:52:11 +05:30
Satish Gandham c06e267206 - Add select component and some cleanup 2024-12-27 13:31:00 +05:30
Satish Gandham 36bf90ca2b Deleted default stories 2024-12-27 13:08:55 +05:30
Satish Gandham 296f579997 Some cleanup 2024-12-20 15:06:25 +05:30
Satish Gandham 2c890af224 Add more tests 2024-12-18 18:17:14 +05:30
Satish Gandham 8d5a549af7 Merge branch 'preview' of https://github.com/makeplane/plane into ui-v2 2024-12-11 13:19:51 +05:30
Satish Gandham c54294388d wip 2024-12-11 13:10:29 +05:30
Satish Gandham 678e06eb62 wip 2024-12-09 12:03:31 +05:30
Satish Gandham 4a7e67dc3d wip 2024-12-06 16:51:09 +05:30
Satish Gandham 526b7061ef Setup unit testing 2024-12-05 18:03:27 +05:30
Satish Gandham 12a499a95a - Add dropdown component
- Setup some tailwind styles
2024-12-04 11:12:51 +05:30
Satish Gandham 171b2c5022 New component library setup with storybook 2024-11-29 16:18:18 +05:30
295 changed files with 72609 additions and 5731 deletions
+3 -3
View File
@@ -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}
+1 -1
View File
@@ -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":
+1 -13
View File
@@ -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:
-12
View File
@@ -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:
+5 -49
View File
@@ -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)
+1 -15
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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',
},
),
]
+9 -4
View File
@@ -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
+27 -90
View File
@@ -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
-32
View File
@@ -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",)
-14
View File
@@ -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"
-3
View File
@@ -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
-100
View File
@@ -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
+1 -1
View File
@@ -70,7 +70,7 @@
"value": ""
},
"GITHUB_CLIENT_SECRET": {
"description": "GitHub Client Secret",
"description": "Github Client Secret",
"value": ""
},
"NEXT_PUBLIC_API_BASE_URL": {
+1 -1
View File
@@ -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
```
+59 -73
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
-1
View File
@@ -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 {
-3
View File
@@ -1,3 +0,0 @@
build/*
dist/*
out/*
-9
View File
@@ -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,
},
};
-5
View File
@@ -1,5 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
-59
View File
@@ -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.
-21
View File
@@ -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"
}
}
-66
View File
@@ -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,
});
-2
View File
@@ -1,2 +0,0 @@
export * from "./config";
export * from "./middleware";
-23
View File
@@ -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();
};
-19
View File
@@ -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
View File
@@ -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;
};
-2
View File
@@ -22,5 +22,3 @@ export type TLogoProps = {
background_color?: string;
};
};
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
-1
View File
@@ -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",
}
-1
View File
@@ -32,4 +32,3 @@ export * from "./workspace-notifications";
export * from "./favorite";
export * from "./file";
export * from "./workspace-draft-issues/base";
export * from "./command-palette";
+2 -3
View File
@@ -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
View File
@@ -1,3 +1,2 @@
export * from "./project_filters";
export * from "./projects";
export * from "./project_link";
-22
View File
@@ -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[];
};
-3
View File
@@ -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
View File
@@ -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;
+25
View File
@@ -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>
+15
View File
@@ -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;
+94
View File
@@ -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: "🍈",
},
];
+73
View File
@@ -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.",
},
];
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
testEnvironment: "jsdom",
moduleNameMapper: {
".(css|less|scss)$": "identity-obj-proxy",
},
};
+54
View File
@@ -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"
}
}
+9
View File
@@ -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" }}>&nbsp;</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_);
View File
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

+640
View File
@@ -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>
+181
View File
@@ -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