Merge pull request #617 from makeplane/sync/ce-ee
sync: community changes
This commit is contained in:
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
@@ -182,7 +182,6 @@ class Issue(ProjectBaseModel):
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# This means that the model isn't saved to the database yet
|
||||
if self.state is None:
|
||||
try:
|
||||
from plane.db.models import State
|
||||
@@ -192,7 +191,6 @@ class Issue(ProjectBaseModel):
|
||||
project=self.project,
|
||||
default=True,
|
||||
).first()
|
||||
# if there is no default state assign any random state
|
||||
if default_state is None:
|
||||
random_state = State.objects.filter(
|
||||
~models.Q(is_triage=True), project=self.project
|
||||
@@ -206,7 +204,6 @@ class Issue(ProjectBaseModel):
|
||||
try:
|
||||
from plane.db.models import State
|
||||
|
||||
# Check if the current issue state group is completed or not
|
||||
if self.state.group == "completed":
|
||||
self.completed_at = timezone.now()
|
||||
else:
|
||||
@@ -215,30 +212,44 @@ class Issue(ProjectBaseModel):
|
||||
pass
|
||||
|
||||
if self._state.adding:
|
||||
# Get the maximum display_id value from the database
|
||||
last_id = IssueSequence.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(largest=models.Max("sequence"))["largest"]
|
||||
# aggregate can return None! Check it first.
|
||||
# If it isn't none, just use the last ID specified (which should be the greatest) and add one to it
|
||||
if last_id:
|
||||
self.sequence_id = last_id + 1
|
||||
else:
|
||||
self.sequence_id = 1
|
||||
with transaction.atomic():
|
||||
last_sequence = (
|
||||
IssueSequence.objects.filter(project=self.project)
|
||||
.select_for_update()
|
||||
.aggregate(largest=models.Max("sequence"))["largest"]
|
||||
)
|
||||
self.sequence_id = last_sequence + 1 if last_sequence else 1
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (
|
||||
self.description_html == ""
|
||||
or self.description_html is None
|
||||
)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
largest_sort_order = Issue.objects.filter(
|
||||
project=self.project, state=self.state
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
|
||||
largest_sort_order = Issue.objects.filter(
|
||||
project=self.project, state=self.state
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
IssueSequence.objects.create(
|
||||
issue=self, sequence=self.sequence_id, project=self.project
|
||||
)
|
||||
else:
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (
|
||||
self.description_html == ""
|
||||
or self.description_html is None
|
||||
)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the issue"""
|
||||
@@ -675,14 +686,3 @@ class IssueVote(ProjectBaseModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.actor.email}"
|
||||
|
||||
|
||||
# TODO: Find a better method to save the model
|
||||
@receiver(post_save, sender=Issue)
|
||||
def create_issue_sequence(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
IssueSequence.objects.create(
|
||||
issue=instance,
|
||||
sequence=instance.sequence_id,
|
||||
project=instance.project,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Disclosure, Transition } from "@headlessui/react";
|
||||
export type TCollapsibleProps = {
|
||||
title: string | React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
isOpen?: boolean;
|
||||
onToggle?: () => void;
|
||||
@@ -11,7 +12,7 @@ export type TCollapsibleProps = {
|
||||
};
|
||||
|
||||
export const Collapsible: FC<TCollapsibleProps> = (props) => {
|
||||
const { title, children, buttonClassName, isOpen, onToggle, defaultOpen } = props;
|
||||
const { title, children, className, buttonClassName, isOpen, onToggle, defaultOpen } = props;
|
||||
// state
|
||||
const [localIsOpen, setLocalIsOpen] = useState<boolean>(isOpen || defaultOpen ? true : false);
|
||||
|
||||
@@ -31,7 +32,7 @@ export const Collapsible: FC<TCollapsibleProps> = (props) => {
|
||||
}, [isOpen, onToggle]);
|
||||
|
||||
return (
|
||||
<Disclosure>
|
||||
<Disclosure as="div" className={className}>
|
||||
<Disclosure.Button className={buttonClassName} onClick={handleOnClick}>
|
||||
{title}
|
||||
</Disclosure.Button>
|
||||
|
||||
@@ -115,7 +115,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
{!noChevron && !disabled && <ChevronDown className="h-3 w-3 flex-shrink-0" aria-hidden="true" />}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ export type TModalVariant = "danger" | "primary";
|
||||
type Props = {
|
||||
content: React.ReactNode | string;
|
||||
handleClose: () => void;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleSubmit: () => void;
|
||||
hideIcon?: boolean;
|
||||
isSubmitting: boolean;
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -79,6 +79,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
onChange={(comment_json, comment_html) => onChange(comment_html)}
|
||||
isSubmitting={isSubmitting}
|
||||
placeholder="Add Comment..."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ const CycleDetailPage = observer(() => {
|
||||
{cycleId && !isSidebarCollapsed && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 vertical-scrollbar scrollbar-sm fixed right-0 z-10"
|
||||
"flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 vertical-scrollbar scrollbar-sm fixed right-0 top-0 z-10"
|
||||
)}
|
||||
style={{
|
||||
boxShadow:
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ const ModuleIssuesPage = observer(() => {
|
||||
{moduleId && !isSidebarCollapsed && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 vertical-scrollbar scrollbar-sm fixed right-0 z-10"
|
||||
"flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 vertical-scrollbar scrollbar-sm fixed right-0 top-0 z-10"
|
||||
)}
|
||||
style={{
|
||||
boxShadow:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
// types
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Collapsible } from "@plane/ui";
|
||||
// components
|
||||
import { DeleteWorkspaceModal } from "@/components/workspace";
|
||||
|
||||
type TDeleteWorkspace = {
|
||||
workspace: IWorkspace | null;
|
||||
};
|
||||
|
||||
export const DeleteWorkspaceSection: FC<TDeleteWorkspace> = observer((props) => {
|
||||
const { workspace } = props;
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteWorkspaceModal
|
||||
data={workspace}
|
||||
isOpen={deleteWorkspaceModal}
|
||||
onClose={() => setDeleteWorkspaceModal(false)}
|
||||
/>
|
||||
<div className="border-t border-custom-border-100">
|
||||
<div className="w-full">
|
||||
<Collapsible
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
className="w-full"
|
||||
buttonClassName="flex w-full items-center justify-between py-4"
|
||||
title={
|
||||
<>
|
||||
<span className="text-lg tracking-tight">Delete Workspace</span>
|
||||
{isOpen ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-base tracking-tight">
|
||||
When deleting a workspace, all of the data and resources within that workspace will be permanently
|
||||
removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setDeleteWorkspaceModal(true)}>
|
||||
Delete my workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./edition-badge";
|
||||
export * from "./upgrade-badge";
|
||||
export * from "./billing";
|
||||
export * from "./delete-workspace-section";
|
||||
|
||||
@@ -37,14 +37,16 @@ export const SelectProject: React.FC<Props> = observer((props) => {
|
||||
onChange={(val: string[]) => onChange(val)}
|
||||
options={options}
|
||||
label={
|
||||
value && value.length > 0
|
||||
? projectIds
|
||||
?.filter((p) => value.includes(p))
|
||||
.map((p) => getProjectById(p)?.name)
|
||||
.join(", ")
|
||||
: "All projects"
|
||||
<div className="truncate">
|
||||
{value && value.length > 0
|
||||
? projectIds
|
||||
?.filter((p) => value.includes(p))
|
||||
.map((p) => getProjectById(p)?.name)
|
||||
.join(", ")
|
||||
: "All projects"}
|
||||
</div>
|
||||
}
|
||||
optionsClassName={"w-48"}
|
||||
optionsClassName="w-48"
|
||||
multiple
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,20 +12,20 @@ import { IWorkspaceSearchResults } from "@plane/types";
|
||||
// hooks
|
||||
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
import {
|
||||
CommandPaletteThemeActions,
|
||||
ChangeIssueAssignee,
|
||||
ChangeIssuePriority,
|
||||
ChangeIssueState,
|
||||
CommandPaletteHelpActions,
|
||||
CommandPaletteIssueActions,
|
||||
CommandPaletteProjectActions,
|
||||
CommandPaletteWorkspaceSettingsActions,
|
||||
CommandPaletteSearchResults,
|
||||
CommandPaletteThemeActions,
|
||||
CommandPaletteWorkspaceSettingsActions,
|
||||
} from "@/components/command-palette";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
|
||||
import { useCommandPalette, useEventTracker, useProject } from "@/hooks/store";
|
||||
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
@@ -46,6 +46,7 @@ export const CommandModal: React.FC = observer(() => {
|
||||
// hooks
|
||||
const { getProjectById, workspaceProjectIds } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { canPerformWorkspaceCreateActions } = useUser();
|
||||
// states
|
||||
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
||||
const [resultsCount, setResultsCount] = useState(0);
|
||||
@@ -282,24 +283,27 @@ export const CommandModal: React.FC = observer(() => {
|
||||
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
|
||||
/>
|
||||
)}
|
||||
{workspaceSlug && workspaceProjectIds && workspaceProjectIds.length > 0 && (
|
||||
<Command.Group heading="Issue">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("Command Palette");
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
className="focus:bg-custom-background-80"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<LayersIcon className="h-3.5 w-3.5" />
|
||||
Create new issue
|
||||
</div>
|
||||
<kbd>C</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
{workspaceSlug &&
|
||||
workspaceProjectIds &&
|
||||
workspaceProjectIds.length > 0 &&
|
||||
canPerformWorkspaceCreateActions && (
|
||||
<Command.Group heading="Issue">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("Command Palette");
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
className="focus:bg-custom-background-80"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<LayersIcon className="h-3.5 w-3.5" />
|
||||
Create new issue
|
||||
</div>
|
||||
<kbd>C</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
{workspaceSlug && (
|
||||
<Command.Group heading="Project">
|
||||
|
||||
@@ -17,8 +17,6 @@ import { CreateProjectModal } from "@/components/project";
|
||||
import { CreateUpdateProjectViewModal } from "@/components/views";
|
||||
// constants
|
||||
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
@@ -43,10 +41,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
const { toggleSidebar } = useAppTheme();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { platform } = usePlatformOS();
|
||||
const {
|
||||
membership: { currentWorkspaceRole, currentProjectRole },
|
||||
data: currentUser,
|
||||
} = useUser();
|
||||
const { data: currentUser, canPerformProjectCreateActions, canPerformWorkspaceCreateActions } = useUser();
|
||||
const {
|
||||
issues: { removeIssue },
|
||||
} = useIssuesStore();
|
||||
@@ -100,30 +95,29 @@ export const CommandPalette: FC = observer(() => {
|
||||
}, [issueId]);
|
||||
|
||||
// auth
|
||||
const canPerformProjectCreateActions = useCallback(
|
||||
const performProjectCreateActions = useCallback(
|
||||
(showToast: boolean = true) => {
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
if (!isAllowed && showToast)
|
||||
if (!canPerformProjectCreateActions && showToast)
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "You don't have permission to perform this action.",
|
||||
});
|
||||
|
||||
return isAllowed;
|
||||
return canPerformProjectCreateActions;
|
||||
},
|
||||
[currentProjectRole]
|
||||
[canPerformProjectCreateActions]
|
||||
);
|
||||
const canPerformWorkspaceCreateActions = useCallback(
|
||||
|
||||
const performWorkspaceCreateActions = useCallback(
|
||||
(showToast: boolean = true) => {
|
||||
const isAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
if (!isAllowed && showToast)
|
||||
if (!canPerformWorkspaceCreateActions && showToast)
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "You don't have permission to perform this action.",
|
||||
});
|
||||
return isAllowed;
|
||||
return canPerformWorkspaceCreateActions;
|
||||
},
|
||||
[currentWorkspaceRole]
|
||||
[canPerformWorkspaceCreateActions]
|
||||
);
|
||||
|
||||
const shortcutsList: {
|
||||
@@ -228,19 +222,20 @@ export const CommandPalette: FC = observer(() => {
|
||||
}
|
||||
} else if (!isAnyModalOpen) {
|
||||
setTrackElement("Shortcut key");
|
||||
if (Object.keys(shortcutsList.global).includes(keyPressed)) shortcutsList.global[keyPressed].action();
|
||||
if (Object.keys(shortcutsList.global).includes(keyPressed) && performWorkspaceCreateActions())
|
||||
shortcutsList.global[keyPressed].action();
|
||||
// workspace authorized actions
|
||||
else if (
|
||||
Object.keys(shortcutsList.workspace).includes(keyPressed) &&
|
||||
workspaceSlug &&
|
||||
canPerformWorkspaceCreateActions()
|
||||
performWorkspaceCreateActions()
|
||||
)
|
||||
shortcutsList.workspace[keyPressed].action();
|
||||
// project authorized actions
|
||||
else if (
|
||||
Object.keys(shortcutsList.project).includes(keyPressed) &&
|
||||
projectId &&
|
||||
canPerformProjectCreateActions()
|
||||
performProjectCreateActions()
|
||||
) {
|
||||
e.preventDefault();
|
||||
// actions that can be performed only inside a project
|
||||
@@ -249,8 +244,8 @@ export const CommandPalette: FC = observer(() => {
|
||||
}
|
||||
},
|
||||
[
|
||||
canPerformProjectCreateActions,
|
||||
canPerformWorkspaceCreateActions,
|
||||
performProjectCreateActions,
|
||||
performWorkspaceCreateActions,
|
||||
copyIssueUrlToClipboard,
|
||||
isAnyModalOpen,
|
||||
projectId,
|
||||
|
||||
@@ -260,7 +260,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
<>
|
||||
<div className="sticky z-10 top-0 flex items-center justify-between bg-custom-sidebar-background-100 py-5">
|
||||
<div className="sticky z-10 pt-20 top-0 flex items-center justify-between bg-custom-sidebar-background-100 pb-5">
|
||||
<div>
|
||||
<button
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-border-300"
|
||||
|
||||
@@ -19,7 +19,7 @@ import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys";
|
||||
import { EXPORTERS_LIST } from "@/constants/workspace";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { IntegrationService } from "@/services/integrations";
|
||||
@@ -38,6 +38,7 @@ const IntegrationGuide = observer(() => {
|
||||
const provider = searchParams.get("provider");
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { workspaceProjectIds } = useProject();
|
||||
|
||||
const { data: exporterServices } = useSWR(
|
||||
workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null,
|
||||
@@ -50,6 +51,8 @@ const IntegrationGuide = observer(() => {
|
||||
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
|
||||
};
|
||||
|
||||
const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full w-full">
|
||||
@@ -73,7 +76,7 @@ const IntegrationGuide = observer(() => {
|
||||
<div className="flex-shrink-0">
|
||||
<Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}>
|
||||
<span>
|
||||
<Button variant="primary" className="capitalize">
|
||||
<Button variant="primary" className="capitalize" disabled={!hasProjects}>
|
||||
{service.type}
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from "./link";
|
||||
export * from "./attachment";
|
||||
export * from "./archived-at";
|
||||
export * from "./inbox";
|
||||
export * from "./label-activity-chip";
|
||||
|
||||
// helpers
|
||||
export * from "./helpers/activity-block";
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { FC } from "react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
|
||||
type TIssueLabelPill = { name?: string; color?: string };
|
||||
|
||||
export const LabelActivityChip: FC<TIssueLabelPill> = (props) => {
|
||||
const { name, color } = props;
|
||||
return (
|
||||
<Tooltip tooltipContent={name}>
|
||||
<span className="inline-flex w-min max-w-32 cursor-default flex-shrink-0 items-center gap-2 truncate whitespace-nowrap rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: color ?? "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="flex-shrink truncate font-medium text-custom-text-100">{name}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { Tag } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail, useLabel } from "@/hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
import { IssueActivityBlockComponent, IssueLink, LabelActivityChip } from "./";
|
||||
|
||||
type TIssueLabelActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
@@ -27,29 +27,14 @@ export const IssueLabelActivity: FC<TIssueLabelActivity> = observer((props) => {
|
||||
>
|
||||
<>
|
||||
{activity.old_value === "" ? `added a new label ` : `removed the label `}
|
||||
{activity.old_value === "" ? (
|
||||
<span className="inline-flex w-min items-center gap-2 truncate whitespace-nowrap rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: projectLabels?.find((l) => l.id === activity.new_identifier)?.color ?? "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="flex-shrink truncate font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex w-min items-center gap-2 truncate whitespace-nowrap rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: projectLabels?.find((l) => l.id === activity.old_identifier)?.color ?? "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="flex-shrink truncate font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
</span>
|
||||
)}
|
||||
<LabelActivityChip
|
||||
name={activity.old_value === "" ? activity.new_value : activity.old_value}
|
||||
color={
|
||||
activity.old_value === ""
|
||||
? projectLabels?.find((l) => l.id === activity.new_identifier)?.color
|
||||
: projectLabels?.find((l) => l.id === activity.old_identifier)?.color
|
||||
}
|
||||
/>
|
||||
{showIssue && (activity.old_value === "" ? ` to ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}
|
||||
</>
|
||||
|
||||
@@ -309,7 +309,7 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
<DeleteModuleModal isOpen={moduleDeleteModal} onClose={() => setModuleDeleteModal(false)} data={moduleDetails} />
|
||||
<>
|
||||
<div className="sticky z-10 top-0 flex items-center justify-between bg-custom-sidebar-background-100 py-5">
|
||||
<div className="sticky z-10 pt-20 top-0 flex items-center justify-between bg-custom-sidebar-background-100 pb-5">
|
||||
<div>
|
||||
<button
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-border-300"
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
import { useEffect, useState, FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { ChevronDown, ChevronUp, Pencil } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { WorkspaceImageUploadModal } from "@/components/core";
|
||||
import { DeleteWorkspaceModal } from "@/components/workspace";
|
||||
// constants
|
||||
import { WORKSPACE_UPDATED } from "@/constants/event-tracker";
|
||||
import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "@/constants/workspace";
|
||||
@@ -19,9 +17,10 @@ import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "@/constants/workspace";
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useUser, useWorkspace } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { DeleteWorkspaceSection } from "@/plane-web/components/workspace";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
// types
|
||||
|
||||
const defaultValues: Partial<IWorkspace> = {
|
||||
name: "",
|
||||
@@ -36,7 +35,6 @@ const fileService = new FileService();
|
||||
export const WorkspaceDetails: FC = observer(() => {
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false);
|
||||
const [isImageRemoving, setIsImageRemoving] = useState(false);
|
||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||
// store hooks
|
||||
@@ -154,11 +152,6 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteWorkspaceModal
|
||||
data={currentWorkspace}
|
||||
isOpen={deleteWorkspaceModal}
|
||||
onClose={() => setDeleteWorkspaceModal(false)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="logo"
|
||||
@@ -308,44 +301,7 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Disclosure as="div" className="border-t border-custom-border-100">
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4">
|
||||
<span className="text-lg tracking-tight">Delete Workspace</span>
|
||||
{/* <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" /> */}
|
||||
{open ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the workspace delete page is a critical area that requires careful
|
||||
consideration and attention. When deleting a workspace, all of the data and resources within
|
||||
that workspace will be permanently removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setDeleteWorkspaceModal(true)}>
|
||||
Delete my workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
{isAdmin && <DeleteWorkspaceSection workspace={currentWorkspace} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import set from "lodash/set";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { action, makeObservable, observable, runInAction, computed } from "mobx";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
@@ -38,6 +41,9 @@ export interface IUserStore {
|
||||
deactivateAccount: () => Promise<void>;
|
||||
reset: () => void;
|
||||
signOut: () => Promise<void>;
|
||||
// computed
|
||||
canPerformProjectCreateActions: boolean;
|
||||
canPerformWorkspaceCreateActions: boolean;
|
||||
}
|
||||
|
||||
export class UserStore implements IUserStore {
|
||||
@@ -82,6 +88,9 @@ export class UserStore implements IUserStore {
|
||||
deactivateAccount: action,
|
||||
reset: action,
|
||||
signOut: action,
|
||||
// computed
|
||||
canPerformProjectCreateActions: computed,
|
||||
canPerformWorkspaceCreateActions: computed,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,4 +228,20 @@ export class UserStore implements IUserStore {
|
||||
await this.authService.signOut(API_BASE_URL);
|
||||
this.store.resetOnSignOut();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description tells if user has project create actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformProjectCreateActions() {
|
||||
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description tells if user has workspace create actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformWorkspaceCreateActions() {
|
||||
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/components/workspace/delete-workspace-section";
|
||||
@@ -2,3 +2,4 @@ export * from "./edition-badge";
|
||||
export * from "./upgrade-badge";
|
||||
export * from "./billing";
|
||||
export * from "./upgrade-toast";
|
||||
export * from "./delete-workspace-section";
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.38.0",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-popper": "^2.3.0",
|
||||
"sharp": "^0.33.4",
|
||||
|
||||
@@ -11398,10 +11398,10 @@ react-fast-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
|
||||
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
|
||||
|
||||
react-hook-form@^7.38.0, react-hook-form@^7.51.0:
|
||||
version "7.52.1"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.1.tgz#ec2c96437b977f8b89ae2d541a70736c66284852"
|
||||
integrity sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==
|
||||
[email protected], react-hook-form@^7.38.0, react-hook-form@^7.51.0:
|
||||
version "7.51.5"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.51.5.tgz#4afbfb819312db9fea23e8237a3a0d097e128b43"
|
||||
integrity sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==
|
||||
|
||||
[email protected]:
|
||||
version "18.1.0"
|
||||
|
||||
Reference in New Issue
Block a user