promote: develop to stage-release (#1589)

* fix: onboarding invitations overflow (#1575)

* fix: onboarding invitations overflow

* fix: user avatar in the notification card

* style: update graph grid color

* fix: no 'Create by me' label coming up (#1573)

* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

* fix: no label for 'Create by me'

* fix: no label for 'Create by me'

* fix: removed console log

* fix: tooltip going behind popover

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* style: tooltip on notification header actions (#1577)

* style: tooltip on notification header

* chore: update tooltip content

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: user migrations for back population (#1578)

* fix: total notifications count (#1579)

* fix: notification card (#1583)

* feat: add new icons package (#1586)

* feat: add material icons package

* chore: replace issue view icons

* chore: notification ordering (#1584)

* fix: uuid error when cycle and module updates (#1585)

* refactor: height of popover & api fetch call (#1587)

* fix: snooze dropdown overflow (#1588)

---------

Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
This commit is contained in:
Aaryan Khandelwal 2023-07-20 15:14:57 +05:30 committed by GitHub
parent b38898753f
commit 9275e6f373
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 736 additions and 1018 deletions

View File

@ -30,7 +30,6 @@ class NotificationViewSet(BaseViewSet):
def list(self, request, slug):
try:
order_by = request.GET.get("order_by", "-created_at")
snoozed = request.GET.get("snoozed", "false")
archived = request.GET.get("archived", "false")
read = request.GET.get("read", "true")
@ -40,7 +39,7 @@ class NotificationViewSet(BaseViewSet):
notifications = Notification.objects.filter(
workspace__slug=slug, receiver_id=request.user.id
).order_by(order_by)
).order_by("snoozed_till", "-created_at")
# Filter for snoozed notifications
if snoozed == "false":

View File

@ -1028,21 +1028,26 @@ def issue_activity(
actor = User.objects.get(pk=actor_id)
project = Project.objects.get(pk=project_id)
if type not in [
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
]:
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
if issue is not None:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
if issue is not None:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
if subscriber:
# add the user to issue subscriber
try:
_ = IssueSubscriber.objects.get_or_create(
issue_id=issue_id, subscriber=actor
)
except Exception as e:
pass
if subscriber:
# add the user to issue subscriber
try:
_ = IssueSubscriber.objects.get_or_create(
issue_id=issue_id, subscriber=actor
)
except Exception as e:
pass
ACTIVITY_MAPPER = {
"issue.activity.created": create_issue_activity,
@ -1094,67 +1099,79 @@ def issue_activity(
except Exception as e:
capture_exception(e)
# Create Notifications
bulk_notifications = []
if type not in [
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
issue_subscribers = list(
IssueSubscriber.objects.filter(project=project, issue_id=issue_id)
.exclude(subscriber_id=actor_id)
.values_list("subscriber", flat=True)
)
issue_subscribers = list(
IssueSubscriber.objects.filter(project=project, issue_id=issue_id)
.exclude(subscriber_id=actor_id)
.values_list("subscriber", flat=True)
)
issue_assignees = list(
IssueAssignee.objects.filter(project=project, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
issue_assignees = list(
IssueAssignee.objects.filter(project=project, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
issue_subscribers = issue_subscribers + issue_assignees
issue_subscribers = issue_subscribers + issue_assignees
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
# Add bot filtering
if issue is not None and issue.created_by_id is not None and not issue.created_by.is_bot:
issue_subscribers = issue_subscribers + [issue.created_by_id]
# Add bot filtering
if (
issue is not None
and issue.created_by_id is not None
and not issue.created_by.is_bot
):
issue_subscribers = issue_subscribers + [issue.created_by_id]
for subscriber in issue_subscribers:
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities",
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
title=issue_activity.comment,
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
for subscriber in issue_subscribers:
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities",
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
title=issue_activity.comment,
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.id),
"verb": str(issue_activity.verb),
"field": str(issue_activity.field),
"actor": str(issue_activity.actor_id),
"new_value": str(issue_activity.new_value),
"old_value": str(issue_activity.old_value),
"issue_comment": str(
issue_activity.issue_comment.comment_stripped
if issue_activity.issue_comment is not None
else ""
),
},
},
"issue_activity": {
"id": str(issue_activity.id),
"verb": str(issue_activity.verb),
"field": str(issue_activity.field),
"actor": str(issue_activity.actor_id),
"new_value": str(issue_activity.new_value),
"old_value": str(issue_activity.old_value),
"issue_comment": str(
issue_activity.issue_comment.comment_stripped if issue_activity.issue_comment is not None else ""
),
},
},
)
)
)
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
return
except Exception as e:

View File

@ -8,6 +8,7 @@ import plane.db.models.user
import uuid
def onboarding_default_steps(apps, schema_editor):
default_onboarding_schema = {
"workspace_join": True,
@ -23,7 +24,7 @@ def onboarding_default_steps(apps, schema_editor):
obj.is_tour_completed = True
updated_user.append(obj)
Model.objects.bulk_update(updated_user, ["onboarding_step"], batch_size=100)
Model.objects.bulk_update(updated_user, ["onboarding_step", "is_tour_completed"], batch_size=100)
class Migration(migrations.Migration):
@ -79,6 +80,7 @@ class Migration(migrations.Migration):
name="onboarding_step",
field=models.JSONField(default=plane.db.models.user.get_default_onboarding),
),
migrations.RunPython(onboarding_default_steps),
migrations.CreateModel(
name="Notification",
fields=[

View File

@ -120,7 +120,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
Please check your inbox at <span className="font-medium">{watch("email")}</span>
</p>
)}
<form className="space-y-4 mt-10">
<form className="space-y-4 mt-10 sm:w-[360px] mx-auto">
<div className="space-y-1">
<Input
id="email"

View File

@ -11,14 +11,16 @@ import useEstimateOption from "hooks/use-estimate-option";
// components
import { SelectFilters } from "components/views";
// ui
import { CustomMenu, Icon, ToggleSwitch, Tooltip } from "components/ui";
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import {
ChevronDownIcon,
ListBulletIcon,
Squares2X2Icon,
CalendarDaysIcon,
} from "@heroicons/react/24/outline";
CalendarMonthOutlined,
FormatListBulletedOutlined,
GridViewOutlined,
TableChartOutlined,
WaterfallChartOutlined,
} from "@mui/icons-material";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
@ -27,26 +29,26 @@ import { Properties, TIssueViewOptions } from "types";
// constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
const issueViewOptions: { type: TIssueViewOptions; icon: any }[] = [
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
icon: <ListBulletIcon className="h-4 w-4" />,
Icon: FormatListBulletedOutlined,
},
{
type: "kanban",
icon: <Squares2X2Icon className="h-4 w-4" />,
Icon: GridViewOutlined,
},
{
type: "calendar",
icon: <CalendarDaysIcon className="h-4 w-4" />,
Icon: CalendarMonthOutlined,
},
{
type: "spreadsheet",
icon: <Icon iconName="table_chart" />,
Icon: TableChartOutlined,
},
{
type: "gantt_chart",
icon: <Icon iconName="waterfall_chart" className="rotate-90" />,
Icon: WaterfallChartOutlined,
},
];
@ -98,7 +100,12 @@ export const IssuesFilterView: React.FC = () => {
}`}
onClick={() => setIssueView(option.type)}
>
{option.icon}
<option.Icon
sx={{
fontSize: 16,
}}
className={option.type === "gantt_chart" ? "rotate-90" : ""}
/>
</button>
</Tooltip>
))}
@ -177,7 +184,6 @@ export const IssuesFilterView: React.FC = () => {
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
width="lg"
>
{GROUP_BY_OPTIONS.map((option) =>
issueView === "kanban" && option.key === null ? null : (
@ -198,7 +204,6 @@ export const IssuesFilterView: React.FC = () => {
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="lg"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
@ -223,7 +228,6 @@ export const IssuesFilterView: React.FC = () => {
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
?.name ?? "Select"
}
width="lg"
>
{FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem

View File

@ -10,7 +10,7 @@ import useToast from "hooks/use-toast";
import { CustomMenu, Icon, Tooltip } from "components/ui";
// helper
import { stripHTML, replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { stripHTML, replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper";
import {
formatDateDistance,
render12HourFormatTime,
@ -32,7 +32,7 @@ type NotificationCardProps = {
const snoozeOptions = [
{
label: "1 days",
label: "1 day",
value: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
},
{
@ -79,102 +79,105 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
`/${workspaceSlug}/projects/${notification.project}/issues/${notification.data.issue.id}`
);
}}
className={`group relative py-3 px-6 cursor-pointer ${
className={`group w-full flex items-center gap-4 p-3 pl-6 relative cursor-pointer ${
notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200"
}`}
>
{notification.read_at === null && (
<span className="absolute top-1/2 left-2 -translate-y-1/2 w-1.5 h-1.5 bg-custom-primary-100 rounded-full" />
)}
<div className="flex items-center gap-4 w-full">
<div className="relative w-12 h-12 rounded-full">
{notification.triggered_by_details.avatar &&
notification.triggered_by_details.avatar !== "" ? (
<div className="relative w-12 h-12 rounded-full">
{notification.triggered_by_details.avatar &&
notification.triggered_by_details.avatar !== "" ? (
<div className="h-12 w-12 rounded-full">
<Image
src={notification.triggered_by_details.avatar}
alt="profile image"
alt="Profile Image"
layout="fill"
objectFit="cover"
className="rounded-full"
/>
) : (
<div className="w-12 h-12 bg-custom-background-100 rounded-full flex justify-center items-center">
<span className="text-custom-text-100 font-medium text-lg">
{notification.triggered_by_details.first_name[0].toUpperCase()}
</span>
</div>
)}
</div>
<div className="w-full space-y-2.5">
<div className="text-sm">
<span className="font-semibold">
{notification.triggered_by_details.first_name}{" "}
{notification.triggered_by_details.last_name}{" "}
</div>
) : (
<div className="w-12 h-12 bg-custom-background-80 rounded-full flex justify-center items-center">
<span className="text-custom-text-100 font-medium text-lg">
{notification.triggered_by_details.first_name[0].toUpperCase()}
</span>
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.verb}{" "}
{notification.data.issue_activity.field === "comment"
? "commented"
: notification.data.issue_activity.field === "None"
? null
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.field !== "None"
? "to"
: ""}
<span className="font-semibold">
{" "}
{notification.data.issue_activity.field !== "None" ? (
notification.data.issue_activity.field !== "comment" ? (
notification.data.issue_activity.field === "target_date" ? (
renderShortDateWithYearFormat(notification.data.issue_activity.new_value)
) : notification.data.issue_activity.field === "attachment" ? (
"the issue"
) : stripHTML(notification.data.issue_activity.new_value).length > 55 ? (
stripHTML(notification.data.issue_activity.new_value).slice(0, 50) + "..."
) : (
stripHTML(notification.data.issue_activity.new_value)
)
</div>
)}
</div>
<div className="space-y-2.5 w-full overflow-hidden">
<div className="text-sm w-full break-words">
<span className="font-semibold">
{notification.triggered_by_details.first_name}{" "}
{notification.triggered_by_details.last_name}{" "}
</span>
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.verb}{" "}
{notification.data.issue_activity.field === "comment"
? "commented"
: notification.data.issue_activity.field === "None"
? null
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.field !== "None"
? "to"
: ""}
<span className="font-semibold">
{" "}
{notification.data.issue_activity.field !== "None" ? (
notification.data.issue_activity.field !== "comment" ? (
notification.data.issue_activity.field === "target_date" ? (
renderShortDateWithYearFormat(notification.data.issue_activity.new_value)
) : notification.data.issue_activity.field === "attachment" ? (
"the issue"
) : stripHTML(notification.data.issue_activity.new_value).length > 55 ? (
stripHTML(notification.data.issue_activity.new_value).slice(0, 50) + "..."
) : (
<span>
{`"`}
{notification.data.issue_activity.new_value.length > 55
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
: notification.data.issue_activity.issue_comment}
{`"`}
</span>
stripHTML(notification.data.issue_activity.new_value)
)
) : (
"the issue and assigned it to you."
)}
</span>
</div>
<div className="w-full flex justify-between text-xs">
<p className="truncate inline max-w-lg text-custom-text-300 mr-3">
{notification.data.issue.identifier}-{notification.data.issue.sequence_id}{" "}
{notification.data.issue.name}
</p>
{notification.snoozed_till ? (
<p className="text-custom-text-300 flex items-center gap-x-1">
<Icon iconName="schedule" />
<span>
Till {renderShortDate(notification.snoozed_till)},{" "}
{render12HourFormatTime(notification.snoozed_till)}
{`"`}
{notification.data.issue_activity.new_value.length > 55
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
: notification.data.issue_activity.issue_comment}
{`"`}
</span>
</p>
)
) : (
<p className="text-custom-text-300">{formatDateDistance(notification.created_at)}</p>
"the issue and assigned it to you."
)}
</div>
</span>
</div>
<div className="flex justify-between gap-2 text-xs">
<p className="text-custom-text-300">
{truncateText(
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
50
)}
</p>
{notification.snoozed_till ? (
<p className="text-custom-text-300 flex items-center justify-end gap-x-1 flex-shrink-0">
<Icon iconName="schedule" className="!text-base -my-0.5" />
<span>
Till {renderShortDate(notification.snoozed_till)},{" "}
{render12HourFormatTime(notification.snoozed_till)}
</span>
</p>
) : (
<p className="text-custom-text-300 flex-shrink-0">
{formatDateDistance(notification.created_at)}
</p>
)}
</div>
</div>
<div className="absolute py-1 gap-x-3 right-3 top-3 hidden group-hover:flex">
{[
{
id: 1,
name: notification.read_at ? "Mark as Unread" : "Mark as Read",
name: notification.read_at ? "Mark as unread" : "Mark as read",
icon: "chat_bubble",
onClick: () => {
markNotificationReadStatus(notification.id).then(() => {
@ -189,8 +192,8 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
},
{
id: 2,
name: notification.archived_at ? "Unarchive Notification" : "Archive Notification",
icon: "archive",
name: notification.archived_at ? "Unarchive" : "Archive",
icon: notification.archived_at ? "unarchive" : "archive",
onClick: () => {
markNotificationArchivedStatus(notification.id).then(() => {
setToastAlert({
@ -203,7 +206,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
},
},
].map((item) => (
<Tooltip tooltipContent={item.name} position="top-left">
<Tooltip tooltipContent={item.name}>
<button
type="button"
onClick={(e) => {
@ -211,52 +214,54 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
item.onClick();
}}
key={item.id}
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded outline-none"
>
<Icon iconName={item.icon} className="h-5 w-5 text-custom-text-300" />
</button>
</Tooltip>
))}
<Tooltip tooltipContent="Snooze Notification" position="top-left">
<CustomMenu
menuButtonOnClick={(e) => {
e.stopPropagation();
}}
customButton={
<button
type="button"
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
>
<Icon iconName="schedule" className="h-5 w-5 text-custom-text-300" />
</button>
}
optionsClassName="!z-20"
>
{snoozeOptions.map((item) => (
<CustomMenu.MenuItem
key={item.label}
renderAs="button"
onClick={(e) => {
e.stopPropagation();
<Tooltip tooltipContent="Snooze">
<div>
<CustomMenu
menuButtonOnClick={(e) => {
e.stopPropagation();
}}
customButton={
<button
type="button"
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
>
<Icon iconName="schedule" className="h-5 w-5 text-custom-text-300" />
</button>
}
optionsClassName="!z-20"
>
{snoozeOptions.map((item) => (
<CustomMenu.MenuItem
key={item.label}
renderAs="button"
onClick={(e) => {
e.stopPropagation();
if (!item.value) {
setSelectedNotificationForSnooze(notification.id);
return;
}
if (!item.value) {
setSelectedNotificationForSnooze(notification.id);
return;
}
markSnoozeNotification(notification.id, item.value).then(() => {
setToastAlert({
title: `Notification snoozed till ${renderLongDateFormat(item.value)}`,
type: "success",
markSnoozeNotification(notification.id, item.value).then(() => {
setToastAlert({
title: `Notification snoozed till ${renderLongDateFormat(item.value)}`,
type: "success",
});
});
});
}}
>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
}}
>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</Tooltip>
</div>
</div>

View File

@ -12,8 +12,10 @@ import useWorkspaceMembers from "hooks/use-workspace-members";
import useUserNotification from "hooks/use-user-notifications";
// components
import { Icon, Loader, EmptyState } from "components/ui";
import { Icon, Loader, EmptyState, Tooltip } from "components/ui";
import { SnoozeNotificationModal, NotificationCard } from "components/notifications";
// icons
import { NotificationsOutlined } from "@mui/icons-material";
// images
import emptyNotification from "public/empty-state/notification.svg";
// helpers
@ -69,7 +71,7 @@ export const NotificationPopover = () => {
{
label: "Subscribed",
value: "watching",
unreadCount: notificationCount?.watching_notifications,
unreadCount: notificationCount?.watching_issues,
},
];
@ -96,10 +98,10 @@ export const NotificationPopover = () => {
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
isActive
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<Icon iconName="notifications" />
<NotificationsOutlined fontSize="small" />
{sidebarCollapse ? null : <span>Notifications</span>}
{totalNotificationCount && totalNotificationCount > 0 ? (
<span className="ml-auto bg-custom-primary-300 rounded-full text-xs text-white px-1.5">
@ -116,54 +118,62 @@ export const NotificationPopover = () => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 md:w-[36rem] w-[20rem] h-[27rem] border border-custom-border-300 shadow-lg rounded-xl">
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 md:w-[36rem] w-[20rem] h-[50vh] border border-custom-border-300 shadow-lg rounded-xl">
<div className="flex items-center justify-between px-5 pt-5">
<h2 className="text-xl font-semibold mb-2">Notifications</h2>
<div className="flex gap-x-4 justify-center items-center text-custom-text-200">
<button
type="button"
onClick={(e) => {
notificationsMutate();
<Tooltip tooltipContent="Refresh">
<button
type="button"
onClick={(e) => {
notificationsMutate();
const target = e.target as HTMLButtonElement;
target?.classList.add("animate-spin");
setTimeout(() => {
target?.classList.remove("animate-spin");
}, 1000);
}}
>
<Icon iconName="refresh" />
</button>
<button
type="button"
onClick={() => {
setSnoozed(false);
setArchived(false);
setReadNotification((prev) => !prev);
}}
>
<Icon iconName="filter_list" />
</button>
<button
type="button"
onClick={() => {
setArchived(false);
setReadNotification(false);
setSnoozed((prev) => !prev);
}}
>
<Icon iconName="schedule" />
</button>
<button
type="button"
onClick={() => {
setSnoozed(false);
setReadNotification(false);
setArchived((prev) => !prev);
}}
>
<Icon iconName="archive" />
</button>
const target = e.target as HTMLButtonElement;
target?.classList.add("animate-spin");
setTimeout(() => {
target?.classList.remove("animate-spin");
}, 1000);
}}
>
<Icon iconName="refresh" />
</button>
</Tooltip>
<Tooltip tooltipContent="Unread notifications">
<button
type="button"
onClick={() => {
setSnoozed(false);
setArchived(false);
setReadNotification((prev) => !prev);
}}
>
<Icon iconName="filter_list" />
</button>
</Tooltip>
<Tooltip tooltipContent="Snoozed notifications">
<button
type="button"
onClick={() => {
setArchived(false);
setReadNotification(false);
setSnoozed((prev) => !prev);
}}
>
<Icon iconName="schedule" />
</button>
</Tooltip>
<Tooltip tooltipContent="Archived notifications">
<button
type="button"
onClick={() => {
setSnoozed(false);
setReadNotification(false);
setArchived((prev) => !prev);
}}
>
<Icon iconName="archive" />
</button>
</Tooltip>
<button type="button" onClick={() => closePopover()}>
<Icon iconName="close" />
</button>
@ -205,6 +215,7 @@ export const NotificationPopover = () => {
: "border-transparent text-custom-text-200"
}`}
>
{tab.label}
{tab.unreadCount && tab.unreadCount > 0 ? (
<span
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
@ -250,7 +261,7 @@ export const NotificationPopover = () => {
{notifications ? (
notifications.length > 0 ? (
<div className="divide-y divide-custom-border-100 overflow-y-auto">
<div className="divide-y divide-custom-border-100 overflow-y-auto h-full">
{notifications.map((notification) => (
<NotificationCard
key={notification.id}
@ -273,7 +284,7 @@ export const NotificationPopover = () => {
</div>
)
) : (
<Loader className="p-5 space-y-4">
<Loader className="p-5 space-y-4 overflow-y-auto">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />

View File

@ -88,7 +88,7 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
<div className="w-full space-y-7 sm:space-y-10">
<h5 className="sm:text-lg">We see that someone has invited you to</h5>
<h4 className="text-xl sm:text-2xl font-semibold">Join a workspace</h4>
<div className="md:w-3/5 space-y-4">
<div className="max-h-[37vh] overflow-y-auto md:w-3/5 space-y-4">
{invitations &&
invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
@ -146,7 +146,11 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
>
Accept & Join
</PrimaryButton>
<SecondaryButton className="border border-none bg-transparent" size="md" onClick={finishOnboarding} >
<SecondaryButton
className="border border-none bg-transparent"
size="md"
onClick={finishOnboarding}
>
Skip for now
</SecondaryButton>
</div>

View File

@ -1,31 +1,37 @@
// ui
import { Icon } from "components/ui";
// icons
import {
ArticleOutlined,
ContrastOutlined,
DatasetOutlined,
FilterNoneOutlined,
PhotoFilterOutlined,
} from "@mui/icons-material";
// types
import { TTourSteps } from "./root";
const sidebarOptions: {
key: TTourSteps;
icon: string;
Icon: any;
}[] = [
{
key: "issues",
icon: "stack",
Icon: FilterNoneOutlined,
},
{
key: "cycles",
icon: "contrast",
Icon: ContrastOutlined,
},
{
key: "modules",
icon: "dataset",
Icon: DatasetOutlined,
},
{
key: "views",
icon: "photo_filter",
Icon: PhotoFilterOutlined,
},
{
key: "pages",
icon: "article",
Icon: ArticleOutlined,
},
];
@ -52,11 +58,10 @@ export const TourSidebar: React.FC<Props> = ({ step, setStep }) => (
}`}
onClick={() => setStep(option.key)}
>
<Icon
iconName={option.icon}
className={`h-5 w-5 flex-shrink-0 ${
step === option.key ? "text-custom-primary-100" : "text-custom-text-200"
}`}
<option.Icon
sx={{
fontSize: 18,
}}
aria-hidden="true"
/>
{option.key}

View File

@ -10,9 +10,19 @@ import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { CustomMenu, Icon, Tooltip } from "components/ui";
import { CustomMenu, Tooltip } from "components/ui";
// icons
import { LinkIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
import {
ArchiveOutlined,
ArticleOutlined,
ContrastOutlined,
DatasetOutlined,
ExpandMoreOutlined,
FilterNoneOutlined,
PhotoFilterOutlined,
SettingsOutlined,
} from "@mui/icons-material";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
@ -33,32 +43,32 @@ const navigation = (workspaceSlug: string, projectId: string) => [
{
name: "Issues",
href: `/${workspaceSlug}/projects/${projectId}/issues`,
icon: "stack",
Icon: FilterNoneOutlined,
},
{
name: "Cycles",
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
icon: "contrast",
Icon: ContrastOutlined,
},
{
name: "Modules",
href: `/${workspaceSlug}/projects/${projectId}/modules`,
icon: "dataset",
Icon: DatasetOutlined,
},
{
name: "Views",
href: `/${workspaceSlug}/projects/${projectId}/views`,
icon: "photo_filter",
Icon: PhotoFilterOutlined,
},
{
name: "Pages",
href: `/${workspaceSlug}/projects/${projectId}/pages`,
icon: "article",
Icon: ArticleOutlined,
},
{
name: "Settings",
href: `/${workspaceSlug}/projects/${projectId}/settings`,
icon: "settings",
Icon: SettingsOutlined,
},
];
@ -164,8 +174,8 @@ export const SingleSidebarProject: React.FC<Props> = ({
)}
</div>
{!sidebarCollapse && (
<Icon
iconName="expand_more"
<ExpandMoreOutlined
fontSize="small"
className={`${open ? "rotate-180" : ""} text-custom-text-200 duration-300`}
/>
)}
@ -211,7 +221,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
}
>
<div className="flex items-center justify-start gap-2">
<Icon iconName="archive" className="h-4 w-4" />
<ArchiveOutlined fontSize="small" />
<span>Archived Issues</span>
</div>
</CustomMenu.MenuItem>
@ -248,13 +258,17 @@ export const SingleSidebarProject: React.FC<Props> = ({
disabled={!sidebarCollapse}
>
<div
className={`group flex items-center rounded-md px-2 py-1.5 gap-2 text-xs font-medium outline-none ${
className={`group flex items-center rounded-md px-2 py-1.5 gap-2.5 text-xs font-medium outline-none ${
router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<Icon iconName={item.icon} />
<item.Icon
sx={{
fontSize: 18,
}}
/>
{!sidebarCollapse && item.name}
</div>
</Tooltip>

View File

@ -1,187 +0,0 @@
import React from "react";
// next
import Link from "next/link";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { Icon } from "./icon";
type Props = {
children: React.ReactNode;
label?: string | JSX.Element;
className?: string;
ellipsis?: boolean;
verticalEllipsis?: boolean;
height?: "sm" | "md" | "rg" | "lg";
width?: "sm" | "md" | "lg" | "xl" | "auto";
textAlignment?: "left" | "center" | "right";
noBorder?: boolean;
noChevron?: boolean;
position?: "left" | "right";
verticalPosition?: "top" | "bottom";
menuItemsClassName?: string;
customButton?: JSX.Element;
menuItemsWhiteBg?: boolean;
};
type MenuItemProps = {
children: JSX.Element | string;
renderAs?: "button" | "a";
href?: string;
onClick?: (args?: any) => void;
className?: string;
};
const CustomMenu = ({
children,
label,
className = "",
ellipsis = false,
verticalEllipsis = false,
height = "md",
width = "auto",
textAlignment,
noBorder = false,
noChevron = false,
position = "right",
verticalPosition = "bottom",
menuItemsClassName = "",
customButton,
menuItemsWhiteBg = false,
}: Props) => (
<Menu as="div" className={`relative w-min whitespace-nowrap text-left ${className}`}>
{({ open }) => (
<>
{customButton ? (
<Menu.Button as="div">{customButton}</Menu.Button>
) : (
<div>
{ellipsis || verticalEllipsis ? (
<Menu.Button
type="button"
className="relative grid place-items-center rounded p-1 text-custom-text-200 hover:bg-custom-background-80 outline-none"
>
<Icon
iconName="more_horiz"
className={`${verticalEllipsis ? "rotate-90" : ""} text-brand-secondary`}
/>
</Menu.Button>
) : (
<Menu.Button
type="button"
className={`flex cursor-pointer items-center justify-between gap-1 px-2.5 py-1 text-xs duration-300 hover:bg-custom-background-80 ${
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
} ${
textAlignment === "right"
? "text-right"
: textAlignment === "center"
? "text-center"
: "text-left"
} ${
noBorder
? "rounded-md"
: "rounded-md border border-custom-border-200 shadow-sm focus:outline-none"
} ${
width === "sm"
? "w-10"
: width === "md"
? "w-20"
: width === "lg"
? "w-32"
: width === "xl"
? "w-48"
: "w-full"
}`}
>
{label}
{!noChevron && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
</Menu.Button>
)}
</div>
)}
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className={`absolute z-20 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
height === "sm"
? "max-h-28"
: height === "md"
? "max-h-44"
: height === "rg"
? "max-h-56"
: height === "lg"
? "max-h-80"
: ""
} ${
width === "sm"
? "w-10"
: width === "md"
? "w-20"
: width === "lg"
? "w-32"
: width === "xl"
? "w-48"
: "min-w-full"
} ${
menuItemsWhiteBg
? "border-custom-border-200 bg-custom-background-100"
: "border-custom-border-200 bg-custom-background-90"
} ${menuItemsClassName}`}
>
<div className="py-1">{children}</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
);
const MenuItem: React.FC<MenuItemProps> = ({
children,
renderAs,
href,
onClick,
className = "",
}) => (
<Menu.Item as="div">
{({ active, close }) =>
renderAs === "a" ? (
<Link href={href ?? ""}>
<a
className={`${className} ${
active ? "bg-custom-background-80" : ""
} hover:text-custom-text-200 inline-block w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
onClick={close}
>
{children}
</a>
</Link>
) : (
<button
type="button"
className={`${className} ${
active ? "bg-custom-background-80" : ""
} hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
onClick={onClick}
>
{children}
</button>
)
}
</Menu.Item>
);
CustomMenu.MenuItem = MenuItem;
export { CustomMenu };

View File

@ -4,9 +4,10 @@ import Link from "next/link";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// ui
import { DropdownProps } from "components/ui";
// icons
import { DropdownProps, Icon } from "components/ui";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { ExpandMoreOutlined, MoreHorizOutlined } from "@mui/icons-material";
export type CustomMenuProps = DropdownProps & {
children: React.ReactNode;
@ -53,8 +54,8 @@ const CustomMenu = ({
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
<Icon
iconName="more_horiz"
<MoreHorizOutlined
fontSize="small"
className={`${verticalEllipsis ? "rotate-90" : ""} text-custom-text-200`}
/>
</Menu.Button>
@ -72,7 +73,14 @@ const CustomMenu = ({
} ${buttonClassName}`}
>
{label}
{!noChevron && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
{!noChevron && (
<ExpandMoreOutlined
sx={{
fontSize: 14,
}}
aria-hidden="true"
/>
)}
</Menu.Button>
)}
</>

View File

@ -1,33 +1,16 @@
import React, { useRef, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr";
// headless ui
import { Transition } from "@headlessui/react";
// services
import workspaceService from "services/workspace.service";
// hooks
import useTheme from "hooks/use-theme";
import useUser from "hooks/use-user";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import UpgradeToProModal from "./upgrade-to-pro-modal";
// ui
import { CircularProgress, Icon } from "components/ui";
// icons
import {
ArrowLongLeftIcon,
ChatBubbleOvalLeftEllipsisIcon,
ArrowUpCircleIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { QuestionMarkCircleIcon, DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
// fetch-keys
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
import { Bolt, HelpOutlineOutlined, WestOutlined } from "@mui/icons-material";
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
import { DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
const helpOptions = [
{
@ -58,150 +41,74 @@ export interface WorkspaceHelpSectionProps {
}
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setSidebarActive }) => {
const [alert, setAlert] = useState(false);
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
const { user } = useUser();
const [upgradeModal, setUpgradeModal] = useState(false);
const { data: workspaceDetails } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.getWorkspace(workspaceSlug as string) : null
);
const issueNumber = workspaceDetails?.total_issues || 0;
return (
<>
<UpgradeToProModal
isOpen={upgradeModal}
onClose={() => setUpgradeModal(false)}
user={user}
issueNumber={issueNumber}
/>
{!sidebarCollapse && (alert || issueNumber >= 750) && (
<div
className={`border-t border-custom-sidebar-border-200 p-4 ${
issueNumber >= 750
? "bg-red-500/10 text-red-600"
: issueNumber >= 500
? "bg-yellow-500/10 text-yellow-600"
: "text-green-600"
}`}
>
<div className="flex items-center gap-2">
<CircularProgress progress={(issueNumber / 1024) * 100} />
<div>Free Plan</div>
{issueNumber < 750 && (
<div
className="ml-auto text-custom-text-200 cursor-pointer"
onClick={() => setAlert(false)}
>
<XMarkIcon className="h-4 w-4" />
</div>
)}
</div>
<div className="text-custom-text-200 text-xs mt-2">
This workspace has used {issueNumber} of its 1024 issues creation limit (
{((issueNumber / 1024) * 100).toFixed(0)}
%).
</div>
</div>
)}
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 py-2 px-4 ${
sidebarCollapse ? "flex-col" : ""
}`}
>
{alert || issueNumber >= 750 ? (
<button
type="button"
className={`flex items-center gap-2 rounded-md px-2.5 py-1.5 font-medium outline-none text-sm ${
issueNumber >= 750
? "bg-red-500/10 text-red-500"
: "bg-blue-500/10 text-custom-primary-100"
} ${sidebarCollapse ? "w-full justify-center" : ""}`}
title="Shortcuts"
onClick={() => setUpgradeModal(true)}
>
<ArrowUpCircleIcon className="h-4 w-4" />
{!sidebarCollapse && <span>Learn more</span>}
</button>
) : (
<button
type="button"
className={`flex items-center gap-2 rounded-md px-2.5 py-1.5 font-medium outline-none text-sm ${
issueNumber >= 750
? "bg-red-500/10 text-red-600"
: issueNumber >= 500
? "bg-yellow-500/10 text-yellow-600"
: "bg-green-500/10 text-green-600"
}
${sidebarCollapse ? "w-full justify-center" : ""}`}
title="Shortcuts"
onClick={() => setAlert(true)}
>
<CircularProgress
progress={(issueNumber / 1024) * 100 > 100 ? 100 : (issueNumber / 1024) * 100}
/>
{!sidebarCollapse && <span>Free Plan</span>}
</button>
{!sidebarCollapse && (
<div className="w-1/2 text-center rounded-md px-2.5 py-1.5 font-medium outline-none text-sm bg-green-500/10 text-green-500">
Free Plan
</div>
)}
<button
type="button"
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
sidebarCollapse ? "w-full justify-center" : ""
<div
className={`flex items-center gap-1 ${
sidebarCollapse ? "flex-col justify-center" : "justify-evenly w-1/2"
}`}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "h",
});
document.dispatchEvent(e);
}}
title="Shortcuts"
>
<Icon iconName="bolt" />
</button>
<button
type="button"
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
sidebarCollapse ? "w-full justify-center" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
title="Help"
>
<QuestionMarkCircleIcon className="h-4 w-4 text-custom-text-200" />
</button>
<button
type="button"
className="flex items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:hidden"
onClick={() => setSidebarActive(false)}
>
<ArrowLongLeftIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200 group-hover:text-custom-text-100" />
</button>
<button
type="button"
className={`hidden items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:flex ${
sidebarCollapse ? "w-full justify-center" : ""
}`}
onClick={() => toggleCollapsed()}
>
<ArrowLongLeftIcon
className={`h-4 w-4 flex-shrink-0 text-custom-text-200 duration-300 group-hover:text-custom-text-100 ${
sidebarCollapse ? "rotate-180" : ""
<button
type="button"
className={`rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
sidebarCollapse ? "w-full" : ""
}`}
/>
</button>
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "h",
});
document.dispatchEvent(e);
}}
>
<Bolt fontSize="small" />
</button>
<button
type="button"
className={`rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
sidebarCollapse ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpOutlineOutlined fontSize="small" />
</button>
<button
type="button"
className="rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none md:hidden"
onClick={() => setSidebarActive(false)}
>
<WestOutlined fontSize="small" />
</button>
<button
type="button"
className={`hidden md:flex rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
sidebarCollapse ? "w-full" : ""
}`}
onClick={() => toggleCollapsed()}
>
<WestOutlined
fontSize="small"
className={`duration-300 ${sidebarCollapse ? "rotate-180" : ""}`}
/>
</button>
</div>
<div className="relative">
<Transition

View File

@ -43,7 +43,7 @@ const userLinks = (workspaceSlug: string) => [
export const WorkspaceSidebarDropdown = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
// fetching user details
const { user, mutateUser } = useUser();
const { collapsed: sidebarCollapse } = useThemeHook();
@ -139,8 +139,8 @@ export const WorkspaceSidebarDropdown = () => {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="fixed left-2 z-20 mt-1 flex w-full max-w-[17rem] origin-top-left flex-col rounded-md
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg focus:outline-none"
className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg outline-none"
>
<div className="flex flex-col items-start justify-start gap-3 p-3">
<div className="text-sm text-custom-sidebar-text-200">{user?.email}</div>

View File

@ -5,40 +5,45 @@ import { useRouter } from "next/router";
// hooks
import useTheme from "hooks/use-theme";
// components
import { NotificationPopover } from "components/notifications";
// ui
import { Tooltip } from "components/ui";
// icons
import {
BarChartRounded,
GridViewOutlined,
TaskAltOutlined,
WorkOutlineOutlined,
} from "@mui/icons-material";
const workspaceLinks = (workspaceSlug: string) => [
{
icon: "grid_view",
Icon: GridViewOutlined,
name: "Dashboard",
href: `/${workspaceSlug}`,
},
{
icon: "bar_chart",
Icon: BarChartRounded,
name: "Analytics",
href: `/${workspaceSlug}/analytics`,
},
{
icon: "work",
Icon: WorkOutlineOutlined,
name: "Projects",
href: `/${workspaceSlug}/projects`,
},
{
icon: "task_alt",
Icon: TaskAltOutlined,
name: "My Issues",
href: `/${workspaceSlug}/me/my-issues`,
},
];
// components
import { Icon, Tooltip } from "components/ui";
export const WorkspaceSidebarMenu = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
// theme context
const { collapsed: sidebarCollapse } = useTheme();
return (
@ -65,7 +70,7 @@ export const WorkspaceSidebarMenu = () => {
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<Icon iconName={`${link.icon}`} />
{<link.Icon fontSize="small" />}
{!sidebarCollapse && link.name}
</div>
</Tooltip>

View File

@ -1,248 +0,0 @@
import React, { useState, useEffect } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// icons
import { XCircleIcon, RocketLaunchIcon } from "@heroicons/react/24/outline";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
// ui
import { CircularProgress } from "components/ui";
// types
import type { ICurrentUserResponse, IWorkspace } from "types";
declare global {
interface Window {
supabase: any;
}
}
type Props = {
isOpen: boolean;
onClose: () => void;
user: ICurrentUserResponse | undefined;
issueNumber: number;
};
const UpgradeToProModal: React.FC<Props> = ({ isOpen, onClose, user, issueNumber }) => {
const [supabaseClient, setSupabaseClient] = useState<any>(null);
useEffect(() => {
// Create a Supabase client
if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
const { createClient } = window.supabase;
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
);
if (supabase) {
setSupabaseClient(supabase);
}
}
}, []);
const [isLoading, setIsLoading] = useState(false);
const handleClose = () => {
onClose();
setIsLoading(false);
};
const proFeatures = [
"Everything in free",
"Unlimited users",
"Unlimited file uploads",
"Priority Support",
"Custom Theming",
"Access to Roadmap",
"Plane AI (GPT unlimited)",
];
const [errorMessage, setErrorMessage] = useState<null | { status: String; message: string }>(
null
);
const [loader, setLoader] = useState(false);
const submitEmail = async () => {
setLoader(true);
const payload = { email: user?.email || "" };
if (supabaseClient) {
if (payload?.email) {
const emailExists = await supabaseClient
.from("web-waitlist")
.select("id,email,count")
.eq("email", payload?.email);
if (emailExists.data.length === 0) {
const emailCreation = await supabaseClient
.from("web-waitlist")
.insert([{ email: payload?.email, count: 1, last_visited: new Date() }])
.select("id,email,count");
if (emailCreation.status === 201)
setErrorMessage({ status: "success", message: "Successfully registered." });
else setErrorMessage({ status: "insert_error", message: "Insertion Error." });
} else {
const emailCountUpdate = await supabaseClient
.from("web-waitlist")
.upsert({
id: emailExists.data[0]?.id,
count: emailExists.data[0]?.count + 1,
last_visited: new Date(),
})
.select("id,email,count");
if (emailCountUpdate.status === 201)
setErrorMessage({
status: "email_already_exists",
message: "Email already exists.",
});
else setErrorMessage({ status: "update_error", message: "Update Error." });
}
} else setErrorMessage({ status: "email_required", message: "Please provide email." });
} else
setErrorMessage({
status: "supabase_error",
message: "Network error. Please try again later.",
});
setLoader(false);
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl">
<div className="flex flex-wrap">
<div className="w-full md:w-3/5 p-6 flex flex-col gap-y-6">
<div className="flex gap-2">
<div
className={`font-semibold outline-none text-sm mt-1.5 ${
issueNumber >= 750
? "text-red-600"
: issueNumber >= 500
? "text-yellow-600"
: "text-green-600"
}`}
title="Shortcuts"
>
<CircularProgress
progress={
(issueNumber / 1024) * 100 > 100 ? 100 : (issueNumber / 1024) * 100
}
/>
</div>
<div className="">
<div className="font-semibold text-lg">Upgrade to pro</div>
<div className="text-custom-text-200 text-sm">
This workspace has used {issueNumber} of its 1024 issues creation limit (
{((issueNumber / 1024) * 100).toFixed(2)}%).
</div>
</div>
<div
onClick={handleClose}
className="w-5 h-5 text-custom-text-200 cursor-pointer mt-1.5 md:hidden block ml-auto"
>
<XCircleIcon />
</div>
</div>
<div className="flex gap-2 pt-6">
<div
className={`font-semibold outline-none text-sm mt-1.5 w-5 h-5 text-[#892FFF] flex-shrink-0`}
title="Shortcuts"
>
<RocketLaunchIcon />
</div>
<div className="">
<div className="font-semibold text-lg">Order summary</div>
<div className="text-custom-text-200 text-sm">
Priority support, file uploads, and access to premium features.
</div>
<div className="flex flex-wrap my-4">
{proFeatures.map((feature, index) => (
<div key={index} className="w-1/2 py-2 flex gap-2 my-1.5">
<div className="w-5 h-5 mt-0.5 text-green-600">
<CheckCircleIcon />
</div>
<div>{feature}</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="w-full md:w-2/5 bg-custom-background-90 p-6 flex flex-col">
<div className="flex justify-between items-center">
<div className="font-semibold text-lg">Summary</div>
<div
onClick={handleClose}
className="w-5 h-5 text-custom-text-200 cursor-pointer mt-1.5 hidden md:block"
>
<XCircleIcon />
</div>
</div>
<div className="text-custom-text-200 text-sm mt-4">
Plane application is currently in dev-mode. We will soon introduce Pro plans
once general availability has been established. Stay tuned for more updates.
In the meantime, Plane remains free and unrestricted.
<br /> <br />
We{"'"}ll ensure a smooth transition from the community version to the Pro
plan for you.
</div>
<button
disabled={loader}
onClick={() => submitEmail()}
type="button"
className="mt-5 md:mt-auto whitespace-nowrap max-w-min items-center gap-x-1 rounded-md px-3 py-2 font-medium outline-none text-sm bg-custom-primary-100 text-white"
>
{loader ? "Loading.." : " Join waitlist"}
</button>
{errorMessage && (
<div
className={`mt-1 text-sm ${
errorMessage && errorMessage?.status === "success"
? "text-green-500"
: " text-red-500"
}`}
>
{errorMessage?.message}
</div>
)}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
export default UpgradeToProModal;

View File

@ -17,12 +17,12 @@ export const CHARTS_THEME: Theme = {
background: "rgb(var(--color-background-80))",
color: "rgb(var(--color-text-200))",
fontSize: "0.8rem",
border: "1px solid rgb(var(--color-background-80))",
border: "1px solid rgb(var(--color-border-300))",
},
},
grid: {
line: {
stroke: "rgb(var(--color-background-80))",
stroke: "rgb(var(--color-border-100))",
},
},
};

View File

@ -217,7 +217,7 @@ export const render12HourFormatTime = (date: string | Date): string => {
if (hours > 12) hours -= 12;
}
return hours + ":" + minutes + " " + period;
return hours + ":" + (minutes < 10 ? `0${minutes}` : minutes) + " " + period;
};
export const render24HourFormatTime = (date: string | Date): string => {

View File

@ -54,44 +54,48 @@ const useUserNotification = () => {
notifications?.find((notification) => notification.id === notificationId)?.read_at !== null;
if (isRead) {
notificationsMutate(
(prev) =>
prev?.map((prevNotification) => {
if (prevNotification.id === notificationId) {
return {
...prevNotification,
read_at: null,
};
}
return prevNotification;
}),
false
);
await userNotificationServices
.markUserNotificationAsUnread(workspaceSlug.toString(), notificationId)
.then(() => {
notificationsMutate((prev) =>
prev?.map((prevNotification) => {
if (prevNotification.id === notificationId) {
return {
...prevNotification,
read_at: null,
};
}
return prevNotification;
})
);
mutateNotificationCount();
})
.catch(() => {
throw new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
});
} else {
notificationsMutate(
(prev) =>
prev?.map((prevNotification) => {
if (prevNotification.id === notificationId) {
return {
...prevNotification,
read_at: new Date(),
};
}
return prevNotification;
}),
false
);
await userNotificationServices
.markUserNotificationAsRead(workspaceSlug.toString(), notificationId)
.then(() => {
notificationsMutate((prev) =>
prev?.map((prevNotification) => {
if (prevNotification.id === notificationId) {
return {
...prevNotification,
read_at: new Date(),
};
}
return prevNotification;
})
);
mutateNotificationCount();
})
.catch(() => {
throw new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
});
}
};
@ -105,22 +109,24 @@ const useUserNotification = () => {
if (isArchived) {
await userNotificationServices
.markUserNotificationAsUnarchived(workspaceSlug.toString(), notificationId)
.then(() => {
notificationsMutate();
})
.catch(() => {
throw new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
});
} else {
notificationsMutate(
(prev) => prev?.filter((prevNotification) => prevNotification.id !== notificationId),
false
);
await userNotificationServices
.markUserNotificationAsArchived(workspaceSlug.toString(), notificationId)
.then(() => {
notificationsMutate((prev) =>
prev?.filter((prevNotification) => prevNotification.id !== notificationId)
);
})
.catch(() => {
throw new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
});
}
};
@ -137,19 +143,25 @@ const useUserNotification = () => {
.patchUserNotification(workspaceSlug.toString(), notificationId, {
snoozed_till: null,
})
.then(() => {
.finally(() => {
notificationsMutate();
});
} else
} else {
notificationsMutate(
(prevData) => prevData?.filter((prev) => prev.id !== notificationId) || [],
false
);
await userNotificationServices
.patchUserNotification(workspaceSlug.toString(), notificationId, {
snoozed_till: dateTime,
})
.then(() => {
notificationsMutate(
(prevData) => prevData?.filter((prev) => prev.id !== notificationId) || []
);
.catch(() => {
new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
});
}
};
return {
@ -170,7 +182,7 @@ const useUserNotification = () => {
setSelectedTab,
totalNotificationCount: notificationCount
? notificationCount.created_issues +
notificationCount.watching_notifications +
notificationCount.watching_issues +
notificationCount.my_issues
: null,
notificationCount,

View File

@ -14,6 +14,8 @@
"@headlessui/react": "^1.7.3",
"@heroicons/react": "^2.0.12",
"@jitsu/nextjs": "^3.1.5",
"@mui/icons-material": "^5.14.1",
"@mui/material": "^5.14.1",
"@nivo/bar": "0.80.0",
"@nivo/calendar": "0.80.0",
"@nivo/core": "0.80.0",

View File

@ -24,7 +24,9 @@ import {
} from "components/workspace";
import { TourRoot } from "components/onboarding";
// ui
import { Icon, PrimaryButton, ProductUpdatesModal } from "components/ui";
import { PrimaryButton, ProductUpdatesModal } from "components/ui";
// icons
import { BoltOutlined, GridViewOutlined } from "@mui/icons-material";
// images
import emptyDashboard from "public/empty-state/dashboard.svg";
import githubBlackImage from "/public/logos/github-black.png";
@ -70,7 +72,7 @@ const WorkspacePage: NextPage = () => {
<WorkspaceAuthorizationLayout
left={
<div className="flex items-center gap-2 pl-3">
<Icon iconName="grid_view" />
<GridViewOutlined fontSize="small" />
Dashboard
</div>
}
@ -80,7 +82,7 @@ const WorkspacePage: NextPage = () => {
onClick={() => setIsProductUpdatesModalOpen(true)}
className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded"
>
<Icon iconName="bolt" className="!text-base -my-1" />
<BoltOutlined fontSize="small" className="-my-1" />
What{"'"}s New?
</button>
<Link href="https://github.com/makeplane/plane" target="_blank" rel="noopener noreferrer">

View File

@ -44,28 +44,18 @@ const BillingSettings: NextPage = () => {
<section className="space-y-8">
<div>
<h3 className="text-2xl font-semibold leading-6">Billing & Plans</h3>
<p className="mt-4 text-sm text-custom-text-200">[Free launch preview] plan Pro</p>
<p className="mt-4 text-sm text-custom-text-200">Free launch preview</p>
</div>
<div className="space-y-8 md:w-2/3">
<div>
<div className="w-80 rounded-md border border-custom-border-200 bg-custom-background-100 p-4 text-center">
<h4 className="text-md mb-1 leading-6">Payment due</h4>
<h2 className="text-3xl font-extrabold">--</h2>
</div>
</div>
<div>
<h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-custom-text-200">
You are currently using the free plan
</p>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<SecondaryButton outline>View Plans and Upgrade</SecondaryButton>
<SecondaryButton outline>View Plans</SecondaryButton>
</a>
</div>
<div>
<h4 className="text-md mb-1 leading-6">Billing history</h4>
<p className="mb-3 text-sm text-custom-text-200">There are no invoices to display</p>
</div>
</div>
</section>
</div>

View File

@ -207,7 +207,10 @@ const WorkspaceSettings: NextPage = () => {
{isImageUploading ? "Uploading..." : "Upload"}
</SecondaryButton>
{activeWorkspace.logo && activeWorkspace.logo !== "" && (
<DangerButton onClick={() => handleDelete(activeWorkspace.logo)}>
<DangerButton
onClick={() => handleDelete(activeWorkspace.logo)}
loading={isImageRemoving}
>
{isImageRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}

View File

@ -145,11 +145,11 @@ const HomePage: NextPage = () => {
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
Sign in to Plane
</h1>
<div className="flex flex-col divide-y divide-custom-border-200 sm:w-[360px] mx-auto">
<div className="flex flex-col divide-y divide-custom-border-200">
<div className="pb-7">
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
</div>
<div className="flex flex-col items-center justify-center gap-4 pt-7 overflow-hidden">
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
<GoogleLoginButton handleSignIn={handleGoogleSignIn} />
<GithubLoginButton handleSignIn={handleGitHubSignIn} />
</div>

View File

@ -105,91 +105,93 @@ const OnBoard: NextPage = () => {
{user?.email}
</div>
</div>
{invitations && invitations.length > 0 ? (
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">
<div className="w-full space-y-10">
<h5 className="text-lg">We see that someone has invited you to</h5>
<h4 className="text-2xl font-semibold">Join a workspace</h4>
<div className="md:w-3/5 space-y-4">
{invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
{invitations ? (
invitations.length > 0 ? (
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">
<div className="w-full space-y-10">
<h5 className="text-lg">We see that someone has invited you to</h5>
<h4 className="text-2xl font-semibold">Join a workspace</h4>
<div className="max-h-[37vh] md:w-3/5 space-y-4 overflow-y-auto">
{invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 border py-5 px-3.5 rounded ${
isSelected
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() =>
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")
}
>
<div className="flex-shrink-0">
<div className="grid place-items-center h-9 w-9 rounded">
{invitation.workspace.logo && invitation.workspace.logo !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid place-items-center h-9 w-9 py-1.5 px-3 rounded bg-gray-700 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">
{truncateText(invitation.workspace.name, 30)}
</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${
isSelected ? "text-custom-primary-100" : "text-custom-text-200"
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 border py-5 px-3.5 rounded ${
isSelected
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() =>
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")
}
>
<CheckCircleIcon className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<PrimaryButton
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
>
Accept & Join
</PrimaryButton>
<Link href="/">
<a>
<SecondaryButton size="md" outline>
Go Home
</SecondaryButton>
</a>
</Link>
<div className="flex-shrink-0">
<div className="grid place-items-center h-9 w-9 rounded">
{invitation.workspace.logo && invitation.workspace.logo !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid place-items-center h-9 w-9 py-1.5 px-3 rounded bg-gray-700 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">
{truncateText(invitation.workspace.name, 30)}
</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${
isSelected ? "text-custom-primary-100" : "text-custom-text-200"
}`}
>
<CheckCircleIcon className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<PrimaryButton
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
>
Accept & Join
</PrimaryButton>
<Link href="/">
<a>
<SecondaryButton size="md" outline>
Go Home
</SecondaryButton>
</a>
</Link>
</div>
</div>
</div>
</div>
) : (
<div className="fixed top-0 left-0 h-full w-full grid place-items-center">
<EmptyState
title="No pending invites"
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
buttonText="Back to Dashboard"
onClick={() => router.push("/")}
/>
</div>
)}
) : (
<div className="fixed top-0 left-0 h-full w-full grid place-items-center">
<EmptyState
title="No pending invites"
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
buttonText="Back to Dashboard"
onClick={() => router.push("/")}
/>
</div>
)
) : null}
</div>
</DefaultLayout>
</UserAuthorizationLayout>

View File

@ -159,7 +159,7 @@ class UserNotificationsServices extends APIService {
async getUnreadNotificationsCount(workspaceSlug: string): Promise<{
created_issues: number;
my_issues: number;
watching_notifications: number;
watching_issues: number;
}> {
return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/unread/`)
.then((response) => response?.data)

View File

@ -295,3 +295,7 @@ body {
:-ms-input-placeholder {
color: rgb(var(--color-text-400));
}
.bp4-overlay-content {
z-index: 555 !important;
}

157
yarn.lock
View File

@ -899,6 +899,13 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438"
integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.18.10", "@babel/template@^7.20.7":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
@ -1005,6 +1012,17 @@
"@emotion/weak-memoize" "^0.3.0"
stylis "4.1.4"
"@emotion/cache@^11.11.0":
version "11.11.0"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff"
integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==
dependencies:
"@emotion/memoize" "^0.8.1"
"@emotion/sheet" "^1.2.2"
"@emotion/utils" "^1.2.1"
"@emotion/weak-memoize" "^0.3.1"
stylis "4.2.0"
"@emotion/css@^11.10.6":
version "11.10.8"
resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.10.8.tgz#9dec9996ad9a1cc28ec8d26b1b27ab0b8f6fb053"
@ -1028,11 +1046,23 @@
dependencies:
"@emotion/memoize" "^0.8.0"
"@emotion/is-prop-valid@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc"
integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==
dependencies:
"@emotion/memoize" "^0.8.1"
"@emotion/memoize@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f"
integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
"@emotion/memoize@^0.8.1":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17"
integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==
"@emotion/react@^11.10.6":
version "11.10.8"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.8.tgz#02e274ecb45e03ab9d7a8eb9f0f0c064613eaf7b"
@ -1063,6 +1093,11 @@
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c"
integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==
"@emotion/sheet@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec"
integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==
"@emotion/styled@^11.10.6":
version "11.10.8"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.8.tgz#a3fd68efd90bd7e8a06b82b95adec643d386fa69"
@ -1090,11 +1125,21 @@
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==
"@emotion/utils@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4"
integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==
"@emotion/weak-memoize@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
"@emotion/weak-memoize@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6"
integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@ -1333,11 +1378,37 @@
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/base@5.0.0-beta.8":
version "5.0.0-beta.8"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.8.tgz#a0a9531ae9147be92d17e4f0e3b9accc57916841"
integrity sha512-b4vVjMZx5KzzEMf4arXKoeV5ZegAMOoPwoy1vfUBwhvXc2QtaaAyBp50U7OA2L06Leubc1A+lEp3eqwZoFn87g==
dependencies:
"@babel/runtime" "^7.22.6"
"@emotion/is-prop-valid" "^1.2.1"
"@mui/types" "^7.2.4"
"@mui/utils" "^5.14.1"
"@popperjs/core" "^2.11.8"
clsx "^1.2.1"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/core-downloads-tracker@^5.12.3":
version "5.12.3"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.12.3.tgz#3dffe62dccc065ddd7338e97d7be4b917004287e"
integrity sha512-yiJZ+knaknPHuRKhRk4L6XiwppwkAahVal3LuYpvBH7GkA2g+D9WLEXOEnNYtVFUggyKf6fWGLGnx0iqzkU5YA==
"@mui/core-downloads-tracker@^5.14.1":
version "5.14.1"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.1.tgz#af156cb3e15b202f5c09f66e7d8b71ca86aef525"
integrity sha512-mIa1WmDmNr1LoupV1Rbxt9bTFKMbIn10RHG1bnZ/FJCkAYpuU/D4n+R+ttiycgcZNngU++zyh/OQeJblzbQPzg==
"@mui/icons-material@^5.14.1":
version "5.14.1"
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.14.1.tgz#2f145c15047a0c7f01353ce620cb88276dadba9e"
integrity sha512-xV/f26muQqtWzerzOIdGPrXoxp/OKaE2G2Wp9gnmG47mHua5Slup/tMc3fA4ZYUreGGrK6+tT81TEvt1Wsng8Q==
dependencies:
"@babel/runtime" "^7.22.6"
"@mui/material@^5.12.1":
version "5.12.3"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.12.3.tgz#398c1b123fb065763558bc1f9fc47d1f8cb87d0c"
@ -1356,6 +1427,24 @@
react-is "^18.2.0"
react-transition-group "^4.4.5"
"@mui/material@^5.14.1":
version "5.14.1"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.14.1.tgz#2711e4ca5c9bdc67b916d01faee650a7a5260bb8"
integrity sha512-WtsgYuageTunLfxH3Ri+o1RuQTFImtRHxMcVNyD0Hhd2/znjW6KODNz0XfjvLRnNCAynBxZNiflcoIBW40h9PQ==
dependencies:
"@babel/runtime" "^7.22.6"
"@mui/base" "5.0.0-beta.8"
"@mui/core-downloads-tracker" "^5.14.1"
"@mui/system" "^5.14.1"
"@mui/types" "^7.2.4"
"@mui/utils" "^5.14.1"
"@types/react-transition-group" "^4.4.6"
clsx "^1.2.1"
csstype "^3.1.2"
prop-types "^15.8.1"
react-is "^18.2.0"
react-transition-group "^4.4.5"
"@mui/private-theming@^5.12.3":
version "5.12.3"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.12.3.tgz#f5e4704e25d9d91b906561cae573cda8f3801e10"
@ -1365,6 +1454,15 @@
"@mui/utils" "^5.12.3"
prop-types "^15.8.1"
"@mui/private-theming@^5.13.7":
version "5.13.7"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.13.7.tgz#2f8ef5da066f3c6c6423bd4260d003a28d10b099"
integrity sha512-qbSr+udcij5F9dKhGX7fEdx2drXchq7htLNr2Qg2Ma+WJ6q0ERlEqGSBiPiVDJkptcjeVL4DGmcf1wl5+vD4EA==
dependencies:
"@babel/runtime" "^7.22.5"
"@mui/utils" "^5.13.7"
prop-types "^15.8.1"
"@mui/styled-engine@^5.12.3":
version "5.12.3"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.12.3.tgz#3307643d52c81947a624cdd0437536cc8109c4f0"
@ -1375,6 +1473,16 @@
csstype "^3.1.2"
prop-types "^15.8.1"
"@mui/styled-engine@^5.13.2":
version "5.13.2"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.13.2.tgz#c87bd61c0ab8086d34828b6defe97c02bcd642ef"
integrity sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==
dependencies:
"@babel/runtime" "^7.21.0"
"@emotion/cache" "^11.11.0"
csstype "^3.1.2"
prop-types "^15.8.1"
"@mui/system@^5.12.3":
version "5.12.3"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.12.3.tgz#306b3cdffa3046067640219c1e5dd7e3dae38ff9"
@ -1389,6 +1497,20 @@
csstype "^3.1.2"
prop-types "^15.8.1"
"@mui/system@^5.14.1":
version "5.14.1"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.14.1.tgz#ec8ae69f63963b5916dad4bca2f8a86a001a2392"
integrity sha512-u+xlsU34Jdkgx1CxmBnIC4Y08uPdVX5iEd3S/1dggDFtOGp+Lj8xmKRJAQ8PJOOJLOh8pDwaZx4AwXikL4l1QA==
dependencies:
"@babel/runtime" "^7.22.6"
"@mui/private-theming" "^5.13.7"
"@mui/styled-engine" "^5.13.2"
"@mui/types" "^7.2.4"
"@mui/utils" "^5.14.1"
clsx "^1.2.1"
csstype "^3.1.2"
prop-types "^15.8.1"
"@mui/types@^7.2.4":
version "7.2.4"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328"
@ -1405,6 +1527,17 @@
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/utils@^5.13.7", "@mui/utils@^5.14.1":
version "5.14.1"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.14.1.tgz#29696371016552a6eb3af975bc7af429ec88b29a"
integrity sha512-39KHKK2JeqRmuUcLDLwM+c2XfVC136C5/yUyQXmO2PVbOb2Bol4KxtkssEqCbTwg87PSCG3f1Tb0keRsK7cVGw==
dependencies:
"@babel/runtime" "^7.22.6"
"@types/prop-types" "^15.7.5"
"@types/react-is" "^18.2.1"
prop-types "^15.8.1"
react-is "^18.2.0"
"@next/env@12.3.2":
version "12.3.2"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.2.tgz#fb819366771f5721e9438ca3a42ad18684f0949b"
@ -1684,6 +1817,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.7.tgz#ccab5c8f7dc557a52ca3288c10075c9ccd37fff7"
integrity sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==
"@popperjs/core@^2.11.8":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@radix-ui/primitive@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253"
@ -3230,6 +3368,13 @@
dependencies:
"@types/react" "^17"
"@types/react-is@^18.2.1":
version "18.2.1"
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-18.2.1.tgz#61d01c2a6fc089a53520c0b66996d458fdc46863"
integrity sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.20":
version "7.1.25"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88"
@ -3247,6 +3392,13 @@
dependencies:
"@types/react" "*"
"@types/react-transition-group@^4.4.6":
version "4.4.6"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e"
integrity sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^18.0.17":
version "18.2.3"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.3.tgz#509ad6c4c77378e686f9bb6e0f8756936392f0e8"
@ -7947,6 +8099,11 @@ stylis@4.1.4:
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.4.tgz#9cb60e7153d8ac6d02d773552bf51c7a0344535b"
integrity sha512-USf5pszRYwuE6hg9by0OkKChkQYEXfkeTtm0xKw+jqQhwyjCVLdYyMBK7R+n7dhzsblAWJnGxju4vxq5eH20GQ==
stylis@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51"
integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
sucrase@^3.32.0:
version "3.32.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.32.0.tgz#c4a95e0f1e18b6847127258a75cf360bc568d4a7"