forked from github/plane
Merge branch 'develop' of github.com:makeplane/plane into develop
This commit is contained in:
commit
f9fa345b25
@ -93,6 +93,7 @@ from plane.api.views import (
|
||||
CompletedCyclesEndpoint,
|
||||
CycleFavoriteViewSet,
|
||||
DraftCyclesEndpoint,
|
||||
TransferCycleIssueEndpoint,
|
||||
## End Cycles
|
||||
# Modules
|
||||
ModuleViewSet,
|
||||
@ -629,6 +630,11 @@ urlpatterns = [
|
||||
),
|
||||
name="user-favorite-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
|
||||
TransferCycleIssueEndpoint.as_view(),
|
||||
name="transfer-issues",
|
||||
),
|
||||
## End Cycles
|
||||
# Issue
|
||||
path(
|
||||
|
@ -51,6 +51,7 @@ from .cycle import (
|
||||
CompletedCyclesEndpoint,
|
||||
CycleFavoriteViewSet,
|
||||
DraftCyclesEndpoint,
|
||||
TransferCycleIssueEndpoint,
|
||||
)
|
||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint
|
||||
from .issue import (
|
||||
|
@ -129,6 +129,36 @@ class CycleViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
if cycle.end_date < timezone.now().date():
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CycleSerializer(cycle, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Cycle.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Cycle does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class CycleIssueViewSet(BaseViewSet):
|
||||
serializer_class = CycleIssueSerializer
|
||||
@ -230,6 +260,14 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
|
||||
if cycle.end_date < timezone.now().date():
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so no new issues can be added"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get all CycleIssues already created
|
||||
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
|
||||
records_to_update = []
|
||||
@ -681,3 +719,60 @@ class CycleFavoriteViewSet(BaseViewSet):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id, cycle_id):
|
||||
try:
|
||||
new_cycle_id = request.data.get("new_cycle_id", False)
|
||||
|
||||
if not new_cycle_id:
|
||||
return Response(
|
||||
{"error": "New Cycle Id is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
new_cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
|
||||
)
|
||||
|
||||
if new_cycle.end_date < timezone.now().date():
|
||||
return Response(
|
||||
{
|
||||
"error": "The cycle where the issues are transferred is already completed"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
cycle_issues = CycleIssue.objects.filter(
|
||||
cycle_id=cycle_id,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
issue__state__group__in=["backlog", "unstarted", "started"],
|
||||
)
|
||||
|
||||
updated_cycles = []
|
||||
for cycle_issue in cycle_issues:
|
||||
cycle_issue.cycle_id = new_cycle_id
|
||||
updated_cycles.append(cycle_issue)
|
||||
|
||||
cycle_issues = CycleIssue.objects.bulk_update(
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
except Cycle.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "New Cycle Does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
@ -28,10 +28,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
prompt = request.data.get("prompt", False)
|
||||
task = request.data.get("task", False)
|
||||
|
||||
if not prompt or not task:
|
||||
if not task:
|
||||
return Response(
|
||||
{"error": "Task and prompt are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
final_text = task + "\n" + prompt
|
||||
@ -45,7 +44,11 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
text = response.choices[0].text.strip()
|
||||
return Response({"response": text}, status=status.HTTP_200_OK)
|
||||
text_html = text.replace("\n", "<br/>")
|
||||
return Response(
|
||||
{"response": text, "response_html": text_html},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
|
@ -26,7 +26,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
return Workspace.objects.filter(
|
||||
q, workspace_member__member=self.request.user
|
||||
).values("name", "id", "slug")
|
||||
).distinct().values("name", "id", "slug")
|
||||
|
||||
def filter_projects(self, query, slug, project_id):
|
||||
fields = ["name"]
|
||||
@ -37,7 +37,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
q,
|
||||
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
||||
workspace__slug=slug,
|
||||
).values("name", "id", "identifier", "workspace__slug")
|
||||
).distinct().values("name", "id", "identifier", "workspace__slug")
|
||||
|
||||
def filter_issues(self, query, slug, project_id):
|
||||
fields = ["name", "sequence_id"]
|
||||
@ -54,7 +54,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).values(
|
||||
).distinct().values(
|
||||
"name",
|
||||
"id",
|
||||
"sequence_id",
|
||||
@ -73,7 +73,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).values(
|
||||
).distinct().values(
|
||||
"name",
|
||||
"id",
|
||||
"project_id",
|
||||
@ -90,7 +90,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).values(
|
||||
).distinct().values(
|
||||
"name",
|
||||
"id",
|
||||
"project_id",
|
||||
@ -107,7 +107,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).values(
|
||||
).distinct().values(
|
||||
"name",
|
||||
"id",
|
||||
"project_id",
|
||||
@ -124,7 +124,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).values(
|
||||
).distinct().values(
|
||||
"name",
|
||||
"id",
|
||||
"project_id",
|
||||
|
@ -6,9 +6,9 @@ import useSWR, { mutate } from "swr";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ChartBarIcon,
|
||||
ChatBubbleOvalLeftEllipsisIcon,
|
||||
ClipboardIcon,
|
||||
FolderPlusIcon,
|
||||
InboxIcon,
|
||||
MagnifyingGlassIcon,
|
||||
Squares2X2Icon,
|
||||
TrashIcon,
|
||||
@ -27,7 +27,7 @@ import {
|
||||
PeopleGroupIcon,
|
||||
SettingIcon,
|
||||
ViewListIcon,
|
||||
PencilScribbleIcon
|
||||
PencilScribbleIcon,
|
||||
} from "components/icons";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
@ -37,6 +37,7 @@ import { Command } from "cmdk";
|
||||
import useTheme from "hooks/use-theme";
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
import useDebounce from "hooks/use-debounce";
|
||||
// components
|
||||
import {
|
||||
ShortcutsModal,
|
||||
@ -50,6 +51,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { CreateUpdateModuleModal } from "components/modules";
|
||||
import { CreateProjectModal } from "components/project";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
import { Spinner } from "components/ui";
|
||||
// helpers
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
@ -58,12 +60,11 @@ import {
|
||||
} from "helpers/string.helper";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import workspaceService from "services/workspace.service";
|
||||
// types
|
||||
import { IIssue, IWorkspaceSearchResults } from "types";
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
import useDebounce from "hooks/use-debounce";
|
||||
import workspaceService from "services/workspace.service";
|
||||
|
||||
export const CommandPalette: React.FC = () => {
|
||||
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
||||
@ -88,7 +89,9 @@ export const CommandPalette: React.FC = () => {
|
||||
page: [],
|
||||
},
|
||||
});
|
||||
const [isPendingAPIRequest, setIsPendingAPIRequest] = useState(false);
|
||||
const [resultsCount, setResultsCount] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||
const [placeholder, setPlaceholder] = React.useState("Type a command or search...");
|
||||
const [pages, setPages] = React.useState<string[]>([]);
|
||||
@ -220,18 +223,39 @@ export const CommandPalette: React.FC = () => {
|
||||
() => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
// this is done prevent api request when user is clearing input
|
||||
setIsLoading(true);
|
||||
// this is done prevent subsequent api request
|
||||
// or searchTerm has not been updated within last 500ms.
|
||||
if (debouncedSearchTerm) {
|
||||
setIsPendingAPIRequest(true);
|
||||
setIsSearching(true);
|
||||
workspaceService
|
||||
.searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm)
|
||||
.then((results) => {
|
||||
setIsPendingAPIRequest(false);
|
||||
setResults(results);
|
||||
const count = Object.keys(results.results).reduce(
|
||||
(accumulator, key) => (results.results as any)[key].length + accumulator,
|
||||
0
|
||||
);
|
||||
setResultsCount(count);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
setIsSearching(false);
|
||||
});
|
||||
} else {
|
||||
setIsPendingAPIRequest(false);
|
||||
setResults({
|
||||
results: {
|
||||
workspace: [],
|
||||
project: [],
|
||||
issue: [],
|
||||
cycle: [],
|
||||
module: [],
|
||||
issue_view: [],
|
||||
page: [],
|
||||
},
|
||||
});
|
||||
setIsLoading(false);
|
||||
setIsSearching(false);
|
||||
}
|
||||
},
|
||||
[debouncedSearchTerm, workspaceSlug, projectId] // Only call effect if debounced search term changes
|
||||
@ -369,11 +393,11 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
{issueId && issueDetails && (
|
||||
<div className="p-3">
|
||||
<span className="rounded-md bg-slate-100 p-1 px-2 text-xs font-medium text-slate-500">
|
||||
<div className="flex p-3">
|
||||
<p className="overflow-hidden truncate rounded-md bg-slate-100 p-1 px-2 text-xs font-medium text-slate-500">
|
||||
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
|
||||
{issueDetails?.name}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative">
|
||||
@ -392,9 +416,20 @@ export const CommandPalette: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<Command.List className="max-h-96 overflow-scroll p-2">
|
||||
<Command.Empty className="my-4 text-center text-gray-500">
|
||||
No results found.
|
||||
</Command.Empty>
|
||||
{!isLoading &&
|
||||
resultsCount === 0 &&
|
||||
searchTerm !== "" &&
|
||||
debouncedSearchTerm !== "" && (
|
||||
<div className="my-4 text-center text-gray-500">No results found.</div>
|
||||
)}
|
||||
|
||||
{(isLoading || isSearching) && (
|
||||
<Command.Loading>
|
||||
<div className="flex h-full w-full items-center justify-center py-8">
|
||||
<Spinner />
|
||||
</div>
|
||||
</Command.Loading>
|
||||
)}
|
||||
|
||||
{debouncedSearchTerm !== "" && (
|
||||
<>
|
||||
@ -419,7 +454,8 @@ export const CommandPalette: React.FC = () => {
|
||||
Icon = AssignmentClipboardIcon;
|
||||
} else if (key === "issue") {
|
||||
path = `/${item.workspace__slug}/projects/${item.project_id}/issues/${item.id}`;
|
||||
value = `${item.project__identifier}-${item.sequence_id} item.name`;
|
||||
// user can search id-num idnum or issue name
|
||||
value = `${item.project__identifier}-${item.sequence_id} ${item.project__identifier}${item.sequence_id} ${item.name}`;
|
||||
Icon = LayerDiagonalIcon;
|
||||
} else if (key === "issue_view") {
|
||||
path = `/${item.workspace__slug}/projects/${item.project_id}/views/${item.id}`;
|
||||
@ -446,9 +482,9 @@ export const CommandPalette: React.FC = () => {
|
||||
className="focus:bg-slate-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-slate-700">
|
||||
<div className="flex items-center gap-2 overflow-hidden text-slate-700">
|
||||
<Icon className="h-4 w-4" />
|
||||
{item.name}
|
||||
<p className="block flex-1 truncate">{item.name}</p>
|
||||
</div>
|
||||
</Command.Item>
|
||||
);
|
||||
@ -720,14 +756,14 @@ export const CommandPalette: React.FC = () => {
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setIsPaletteOpen(false);
|
||||
window.open("mailto:hello@plane.so", "_blank");
|
||||
(window as any).$crisp.push(["do", "chat:open"]);
|
||||
}}
|
||||
className="focus:bg-slate-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-slate-700">
|
||||
<InboxIcon className="h-4 w-4" />
|
||||
Email us
|
||||
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4" />
|
||||
Chat with us
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -16,6 +17,7 @@ type Props = {
|
||||
handleClose: () => void;
|
||||
inset?: string;
|
||||
content: string;
|
||||
htmlContent?: string;
|
||||
onResponse: (response: string) => void;
|
||||
};
|
||||
|
||||
@ -24,11 +26,16 @@ type FormData = {
|
||||
task: string;
|
||||
};
|
||||
|
||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const GptAssistantModal: React.FC<Props> = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
inset = "top-0 left-0",
|
||||
content,
|
||||
htmlContent,
|
||||
onResponse,
|
||||
}) => {
|
||||
const [response, setResponse] = useState("");
|
||||
@ -62,15 +69,6 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
const handleResponse = async (formData: FormData) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
if (!content || content === "") {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Please enter some description to get AI assistance.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.task === "") {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@ -82,11 +80,11 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
|
||||
await aiService
|
||||
.createGptTask(workspaceSlug as string, projectId as string, {
|
||||
prompt: content,
|
||||
prompt: content && content !== "" ? content : "",
|
||||
task: formData.task,
|
||||
})
|
||||
.then((res) => {
|
||||
setResponse(res.response);
|
||||
setResponse(res.response_html);
|
||||
setFocus("task");
|
||||
|
||||
if (res.response === "") setInvalidResponse(true);
|
||||
@ -105,12 +103,28 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleResponse)} className="space-y-4">
|
||||
{content && content !== "" && (
|
||||
<div className="text-sm">
|
||||
Content: <p className="text-gray-500">{content}</p>
|
||||
Content:
|
||||
<RemirrorRichTextEditor
|
||||
value={htmlContent ?? <p>{content}</p>}
|
||||
customClassName="-mx-3 -my-3"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
editable={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{response !== "" && (
|
||||
<div className="text-sm">
|
||||
Response: <p className="text-gray-500">{response}</p>
|
||||
Response:
|
||||
<RemirrorRichTextEditor
|
||||
value={`<p>${response}</p>`}
|
||||
customClassName="-mx-3 -my-3"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
editable={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{invalidResponse && (
|
||||
@ -123,7 +137,11 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
type="text"
|
||||
name="task"
|
||||
register={register}
|
||||
placeholder="Tell OpenAI what action to perform on this content..."
|
||||
placeholder={`${
|
||||
content && content !== ""
|
||||
? "Tell AI what action to perform on this content..."
|
||||
: "Ask AI anything..."
|
||||
}`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
|
||||
|
@ -30,7 +30,7 @@ import {
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { ExclamationIcon, getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
@ -683,6 +683,12 @@ export const IssuesView: React.FC<Props> = ({
|
||||
{groupedByIssues ? (
|
||||
isNotEmpty ? (
|
||||
<>
|
||||
{isCompleted && (
|
||||
<div className="flex items-center gap-2 text-sm mb-4 text-gray-500">
|
||||
<ExclamationIcon height={14} width={14} />
|
||||
<span>Completed cycles are not editable.</span>
|
||||
</div>
|
||||
)}
|
||||
{issueView === "list" ? (
|
||||
<AllLists
|
||||
type={type}
|
||||
|
@ -13,12 +13,11 @@ import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// components
|
||||
import { LinksList, SingleProgressStats } from "components/core";
|
||||
import { SingleProgressStats } from "components/core";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
// icons
|
||||
import User from "public/user.png";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IIssueLabels, IModule, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
@ -28,8 +27,6 @@ type Props = {
|
||||
groupedIssues: any;
|
||||
issues: IIssue[];
|
||||
module?: IModule;
|
||||
setModuleLinkModal?: any;
|
||||
handleDeleteLink?: any;
|
||||
userAuth?: UserAuth;
|
||||
};
|
||||
|
||||
@ -47,8 +44,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
groupedIssues,
|
||||
issues,
|
||||
module,
|
||||
setModuleLinkModal,
|
||||
handleDeleteLink,
|
||||
userAuth,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -72,14 +67,12 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
|
||||
const currentValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "Links":
|
||||
return 0;
|
||||
case "Assignees":
|
||||
return 1;
|
||||
return 0;
|
||||
case "Labels":
|
||||
return 2;
|
||||
return 1;
|
||||
case "States":
|
||||
return 3;
|
||||
return 2;
|
||||
|
||||
default:
|
||||
return 3;
|
||||
@ -91,12 +84,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setTab("Links");
|
||||
case 1:
|
||||
return setTab("Assignees");
|
||||
case 2:
|
||||
case 1:
|
||||
return setTab("Labels");
|
||||
case 3:
|
||||
case 2:
|
||||
return setTab("States");
|
||||
|
||||
default:
|
||||
@ -109,20 +100,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
className={`flex w-full items-center justify-between rounded-md bg-gray-100 px-1 py-1.5
|
||||
${module ? "text-xs" : "text-sm"} `}
|
||||
>
|
||||
{module ? (
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full rounded px-3 py-1 text-gray-900 ${
|
||||
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Links
|
||||
</Tab>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full rounded px-3 py-1 text-gray-900 ${
|
||||
@ -152,29 +129,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="flex w-full items-center justify-between p-1">
|
||||
{module ? (
|
||||
<Tab.Panel as="div" className="flex w-full flex-col text-xs ">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-start gap-2 rounded px-4 py-2 hover:bg-theme/5"
|
||||
onClick={() => setModuleLinkModal(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" /> <span>Add Link</span>
|
||||
</button>
|
||||
<div className="mt-2 space-y-2 hover:bg-theme/5">
|
||||
{userAuth && module.link_module && module.link_module.length > 0 ? (
|
||||
<LinksList
|
||||
links={module.link_module}
|
||||
handleDeleteLink={handleDeleteLink}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
<Tab.Panel as="div" className="flex w-full flex-col text-xs ">
|
||||
{members?.map((member, index) => {
|
||||
const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id));
|
||||
|
@ -9,7 +9,7 @@ import cyclesService from "services/cycles.service";
|
||||
// components
|
||||
import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
|
||||
// icons
|
||||
import { CompletedCycleIcon } from "components/icons";
|
||||
import { CompletedCycleIcon, ExclamationIcon } from "components/icons";
|
||||
// types
|
||||
import { ICycle, SelectCycleType } from "types";
|
||||
// fetch-keys
|
||||
@ -63,6 +63,11 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
|
||||
/>
|
||||
{completedCycles ? (
|
||||
completedCycles.completed_cycles.length > 0 ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<ExclamationIcon height={14} width={14} />
|
||||
<span>Completed cycles are not editable.</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{completedCycles.completed_cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
@ -74,6 +79,7 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
|
@ -239,7 +239,11 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
control={control}
|
||||
render={({ field: { value } }) => (
|
||||
<RemirrorRichTextEditor
|
||||
value={value}
|
||||
value={
|
||||
!value || (typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("description_html")
|
||||
: value
|
||||
}
|
||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||
placeholder="Description"
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
ChevronDownIcon,
|
||||
DocumentDuplicateIcon,
|
||||
DocumentIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
@ -24,7 +25,7 @@ import modulesService from "services/modules.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { LinkModal, SidebarProgressStats } from "components/core";
|
||||
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
|
||||
import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules";
|
||||
import ProgressChart from "components/core/sidebar/progress-chart";
|
||||
import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui";
|
||||
@ -414,7 +415,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{isStartValid && isEndValid ? (
|
||||
<Disclosure.Button>
|
||||
<Disclosure.Button className="p-1">
|
||||
<ChevronDownIcon
|
||||
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
|
||||
aria-hidden="true"
|
||||
@ -485,7 +486,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{issues.length > 0 ? (
|
||||
<Disclosure.Button>
|
||||
<Disclosure.Button className="p-1">
|
||||
<ChevronDownIcon
|
||||
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
|
||||
aria-hidden="true"
|
||||
@ -508,8 +509,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
|
||||
<SidebarProgressStats
|
||||
issues={issues}
|
||||
groupedIssues={groupedIssues}
|
||||
setModuleLinkModal={setModuleLinkModal}
|
||||
handleDeleteLink={handleDeleteLink}
|
||||
userAuth={userAuth}
|
||||
module={module}
|
||||
/>
|
||||
@ -524,6 +523,27 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col text-xs border-t border-gray-300 px-6 py-6">
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<h4 className="font-medium text-sm text-gray-500">Links</h4>
|
||||
<button
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100"
|
||||
onClick={() => setModuleLinkModal(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-2 hover:bg-gray-100">
|
||||
{userAuth && module.link_module && module.link_module.length > 0 ? (
|
||||
<LinksList
|
||||
links={module.link_module}
|
||||
handleDeleteLink={handleDeleteLink}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="px-5">
|
||||
|
@ -45,12 +45,20 @@ export const CreateUpdatePageModal: React.FC<Props> = ({ isOpen, handleClose, da
|
||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||
mutate<IPage[]>(
|
||||
MY_PAGES_LIST(projectId as string),
|
||||
(prevData) => [res, ...(prevData as IPage[])],
|
||||
(prevData) => {
|
||||
if (!prevData) return undefined;
|
||||
|
||||
return [res, ...(prevData as IPage[])];
|
||||
},
|
||||
false
|
||||
);
|
||||
mutate<IPage[]>(
|
||||
ALL_PAGES_LIST(projectId as string),
|
||||
(prevData) => [res, ...(prevData as IPage[])],
|
||||
(prevData) => {
|
||||
if (!prevData) return undefined;
|
||||
|
||||
return [res, ...(prevData as IPage[])];
|
||||
},
|
||||
false
|
||||
);
|
||||
onClose();
|
||||
|
@ -2,6 +2,8 @@ import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
@ -14,6 +16,13 @@ import { DangerButton, SecondaryButton } from "components/ui";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { IPage } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
ALL_PAGES_LIST,
|
||||
FAVORITE_PAGES_LIST,
|
||||
MY_PAGES_LIST,
|
||||
RECENT_PAGES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type TConfirmPageDeletionProps = {
|
||||
isOpen: boolean;
|
||||
@ -45,6 +54,22 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = ({
|
||||
await pagesService
|
||||
.deletePage(workspaceSlug as string, data.project, data.id)
|
||||
.then(() => {
|
||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||
mutate<IPage[]>(
|
||||
MY_PAGES_LIST(projectId as string),
|
||||
(prevData) => (prevData ?? []).filter((page) => page.id !== data?.id),
|
||||
false
|
||||
);
|
||||
mutate<IPage[]>(
|
||||
ALL_PAGES_LIST(projectId as string),
|
||||
(prevData) => (prevData ?? []).filter((page) => page.id !== data?.id),
|
||||
false
|
||||
);
|
||||
mutate<IPage[]>(
|
||||
FAVORITE_PAGES_LIST(projectId as string),
|
||||
(prevData) => (prevData ?? []).filter((page) => page.id !== data?.id),
|
||||
false
|
||||
);
|
||||
handleClose();
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
|
@ -57,7 +57,6 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
||||
const handleAddToFavorites = (page: IPage) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||
mutate<IPage[]>(
|
||||
ALL_PAGES_LIST(projectId as string),
|
||||
(prevData) =>
|
||||
@ -89,6 +88,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
||||
page: page.id,
|
||||
})
|
||||
.then(() => {
|
||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
@ -107,7 +107,6 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
||||
const handleRemoveFromFavorites = (page: IPage) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||
mutate<IPage[]>(
|
||||
ALL_PAGES_LIST(projectId as string),
|
||||
(prevData) =>
|
||||
@ -137,6 +136,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
||||
pagesService
|
||||
.removePageFromFavorites(workspaceSlug as string, projectId as string, page.id)
|
||||
.then(() => {
|
||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
|
@ -17,11 +17,16 @@ import useToast from "hooks/use-toast";
|
||||
import { CreateUpdateIssueModal } from "components/issues";
|
||||
import { GptAssistantModal } from "components/core";
|
||||
// ui
|
||||
import { CustomMenu, Loader, TextArea } from "components/ui";
|
||||
import { CustomMenu, Input, Loader, TextArea } from "components/ui";
|
||||
// icons
|
||||
import { LayerDiagonalIcon, WaterDropIcon } from "components/icons";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
BoltIcon,
|
||||
CheckIcon,
|
||||
CursorArrowRaysIcon,
|
||||
SparklesIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -163,21 +168,8 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
const handleAiAssistance = async (response: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
setValue("description", {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
text: response,
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
setValue("description_html", `<p>${response}</p>`);
|
||||
setValue("description", {});
|
||||
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
|
||||
handleSubmit(updatePageBlock)()
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
@ -253,7 +245,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
}}
|
||||
/>
|
||||
<div className="-mx-3 -mt-2 flex items-center justify-between gap-2">
|
||||
<TextArea
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Block title"
|
||||
@ -261,11 +253,11 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
onBlur={handleSubmit(updatePageBlock)}
|
||||
onChange={(e) => setValue("name", e.target.value)}
|
||||
required={true}
|
||||
className="min-h-10 block w-full resize-none overflow-hidden border-none bg-transparent text-base font-medium"
|
||||
className="min-h-10 block w-full resize-none overflow-hidden border-none bg-transparent py-1 text-base font-medium ring-0 focus:ring-1 focus:ring-gray-200"
|
||||
role="textbox"
|
||||
/>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
{block.sync && (
|
||||
{block.issue && block.sync && (
|
||||
<div className="flex flex-shrink-0 cursor-default items-center gap-1 rounded bg-gray-100 py-1 px-1.5 text-xs">
|
||||
{isSyncing ? (
|
||||
<ArrowPathIcon className="h-3 w-3 animate-spin" />
|
||||
@ -285,12 +277,13 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="-mr-2 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
||||
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||
>
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
AI
|
||||
</button>
|
||||
<CustomMenu label={<WaterDropIcon width={14} height={15} />} noBorder noChevron>
|
||||
<CustomMenu label={<BoltIcon className="h-4.5 w-3.5" />} noBorder noChevron>
|
||||
{block.issue ? (
|
||||
<>
|
||||
<CustomMenu.MenuItem onClick={handleBlockSync}>
|
||||
@ -312,7 +305,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-block-section relative -mx-3 -mt-5">
|
||||
<div className="page-block-section font relative -mx-3 -mt-3">
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
@ -327,8 +320,9 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||
placeholder="Block description..."
|
||||
customClassName="text-gray-500"
|
||||
customClassName="border border-transparent"
|
||||
noBorder
|
||||
borderOnFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -337,6 +331,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
handleClose={() => setGptAssistantModal(false)}
|
||||
inset="top-2 left-0"
|
||||
content={block.description_stripped}
|
||||
htmlContent={block.description_html}
|
||||
onResponse={handleAiAssistance}
|
||||
/>
|
||||
</div>
|
||||
|
@ -6,13 +6,12 @@ import { useRouter } from "next/router";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { DocumentTextIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IPage } from "types";
|
||||
import { PencilScribbleIcon } from "components/icons";
|
||||
|
||||
type TSingleStatProps = {
|
||||
page: IPage;
|
||||
@ -39,7 +38,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
||||
<div className="relative rounded p-4 hover:bg-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<PencilScribbleIcon />
|
||||
<DocumentTextIcon className="h-4 w-4" />
|
||||
<p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
|
||||
{page.label_details.length > 0 &&
|
||||
page.label_details.map((label) => (
|
||||
|
@ -6,7 +6,7 @@ import { Disclosure, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { ChevronDownIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
ContrastIcon,
|
||||
LayerDiagonalIcon,
|
||||
@ -53,7 +53,7 @@ const navigation = (workspaceSlug: string, projectId: string) => [
|
||||
{
|
||||
name: "Pages",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
||||
icon: PencilScribbleIcon,
|
||||
icon: DocumentTextIcon,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
|
@ -50,6 +50,7 @@ export interface IRemirrorRichTextEditor {
|
||||
customClassName?: string;
|
||||
gptOption?: boolean;
|
||||
noBorder?: boolean;
|
||||
borderOnFocus?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
@ -69,6 +70,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
customClassName,
|
||||
gptOption = false,
|
||||
noBorder = false,
|
||||
borderOnFocus = true,
|
||||
} = props;
|
||||
|
||||
const [imageLoader, setImageLoader] = useState(false);
|
||||
@ -188,9 +190,9 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
manager={manager}
|
||||
initialContent={state}
|
||||
classNames={[
|
||||
`p-4 relative focus:outline-none rounded-md focus:border-theme ${
|
||||
`p-4 relative focus:outline-none rounded-md focus:border-gray-200 ${
|
||||
noBorder ? "" : "border"
|
||||
} ${customClassName}`,
|
||||
} ${borderOnFocus ? "focus:border" : ""} ${customClassName}`,
|
||||
]}
|
||||
editable={editable}
|
||||
onBlur={() => {
|
||||
|
@ -60,7 +60,7 @@ export const CompletedIssuesGraph: React.FC<Props> = ({ month, issues, setMonth
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid stroke="#e2e2e2" />
|
||||
<XAxis dataKey="week_in_month" />
|
||||
<YAxis dataKey="completed_count" />
|
||||
<YAxis dataKey="completed_count" allowDecimals={false} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Line
|
||||
type="monotone"
|
||||
|
@ -8,14 +8,12 @@ import { Transition } from "@headlessui/react";
|
||||
import useTheme from "hooks/use-theme";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { ArrowLongLeftIcon, ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
QuestionMarkCircleIcon,
|
||||
BoltIcon,
|
||||
DocumentIcon,
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
} from "components/icons";
|
||||
ArrowLongLeftIcon,
|
||||
ChatBubbleOvalLeftEllipsisIcon,
|
||||
RocketLaunchIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { QuestionMarkCircleIcon, DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
|
||||
|
||||
const helpOptions = [
|
||||
{
|
||||
@ -77,7 +75,7 @@ export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
|
||||
}}
|
||||
title="Shortcuts"
|
||||
>
|
||||
<BoltIcon className="h-4 w-4 text-gray-500" />
|
||||
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
|
||||
{!sidebarCollapse && <span>Shortcuts</span>}
|
||||
</button>
|
||||
<button
|
||||
|
@ -23,95 +23,95 @@ export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
||||
export const USER_WORKSPACES = "USER_WORKSPACES";
|
||||
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
|
||||
|
||||
export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug}`;
|
||||
export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug.toUpperCase()}`;
|
||||
export const WORKSPACE_INTEGRATIONS = (workspaceSlug: string) =>
|
||||
`WORKSPACE_INTEGRATIONS_${workspaceSlug}`;
|
||||
`WORKSPACE_INTEGRATIONS_${workspaceSlug.toUpperCase()}`;
|
||||
|
||||
export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug}`;
|
||||
export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug.toUpperCase()}`;
|
||||
export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) =>
|
||||
`WORKSPACE_MEMBERS_ME${workspaceSlug}`;
|
||||
`WORKSPACE_MEMBERS_ME${workspaceSlug.toUpperCase()}`;
|
||||
export const WORKSPACE_INVITATIONS = "WORKSPACE_INVITATIONS";
|
||||
export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION";
|
||||
export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "LAST_ACTIVE_WORKSPACE_AND_PROJECTS";
|
||||
|
||||
export const PROJECTS_LIST = (workspaceSlug: string) => `PROJECTS_LIST_${workspaceSlug}`;
|
||||
export const PROJECTS_LIST = (workspaceSlug: string) => `PROJECTS_LIST_${workspaceSlug.toUpperCase()}`;
|
||||
export const FAVORITE_PROJECTS_LIST = (workspaceSlug: string) =>
|
||||
`FAVORITE_PROJECTS_LIST_${workspaceSlug}`;
|
||||
export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId}`;
|
||||
`FAVORITE_PROJECTS_LIST_${workspaceSlug.toUpperCase()}`;
|
||||
export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId.toUpperCase()}`;
|
||||
|
||||
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId}`;
|
||||
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId.toUpperCase()}`;
|
||||
export const PROJECT_INVITATIONS = "PROJECT_INVITATIONS";
|
||||
|
||||
export const PROJECT_ISSUES_LIST = (workspaceSlug: string, projectId: string) =>
|
||||
`PROJECT_ISSUES_LIST_${workspaceSlug}_${projectId}`;
|
||||
`PROJECT_ISSUES_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}`;
|
||||
export const PROJECT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
|
||||
if (!params) return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId}`;
|
||||
if (!params) return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}`;
|
||||
|
||||
const paramsKey = paramsToKey(params);
|
||||
|
||||
return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId}_${paramsKey}`;
|
||||
return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
|
||||
};
|
||||
export const PROJECT_ISSUES_DETAILS = (issueId: string) => `PROJECT_ISSUES_DETAILS_${issueId}`;
|
||||
export const PROJECT_ISSUES_DETAILS = (issueId: string) => `PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
||||
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
||||
`PROJECT_ISSUES_PROPERTIES_${projectId}`;
|
||||
export const PROJECT_ISSUES_COMMENTS = (issueId: string) => `PROJECT_ISSUES_COMMENTS_${issueId}`;
|
||||
export const PROJECT_ISSUES_ACTIVITY = (issueId: string) => `PROJECT_ISSUES_ACTIVITY_${issueId}`;
|
||||
export const PROJECT_ISSUE_BY_STATE = (projectId: string) => `PROJECT_ISSUE_BY_STATE_${projectId}`;
|
||||
export const PROJECT_ISSUE_LABELS = (projectId: string) => `PROJECT_ISSUE_LABELS_${projectId}`;
|
||||
`PROJECT_ISSUES_PROPERTIES_${projectId.toUpperCase()}`;
|
||||
export const PROJECT_ISSUES_COMMENTS = (issueId: string) => `PROJECT_ISSUES_COMMENTS_${issueId.toUpperCase()}`;
|
||||
export const PROJECT_ISSUES_ACTIVITY = (issueId: string) => `PROJECT_ISSUES_ACTIVITY_${issueId.toUpperCase()}`;
|
||||
export const PROJECT_ISSUE_BY_STATE = (projectId: string) => `PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`;
|
||||
export const PROJECT_ISSUE_LABELS = (projectId: string) => `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`;
|
||||
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
|
||||
`PROJECT_GITHUB_REPOSITORY_${projectId}`;
|
||||
`PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`;
|
||||
|
||||
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`;
|
||||
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`;
|
||||
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`;
|
||||
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId.toUpperCase()}`;
|
||||
export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => {
|
||||
if (!params) return `CYCLE_ISSUES_WITH_PARAMS_${cycleId}`;
|
||||
if (!params) return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}`;
|
||||
|
||||
const paramsKey = paramsToKey(params);
|
||||
|
||||
return `CYCLE_ISSUES_WITH_PARAMS_${cycleId}_${paramsKey}`;
|
||||
return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}_${paramsKey.toUpperCase()}`;
|
||||
};
|
||||
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId}`;
|
||||
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`;
|
||||
export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) =>
|
||||
`CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId}`;
|
||||
export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId}`;
|
||||
export const CYCLE_COMPLETE_LIST = (projectId: string) => `CYCLE_COMPLETE_LIST_${projectId}`;
|
||||
`CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId.toUpperCase()}`;
|
||||
export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId.toUpperCase()}`;
|
||||
export const CYCLE_COMPLETE_LIST = (projectId: string) => `CYCLE_COMPLETE_LIST_${projectId.toUpperCase()}`;
|
||||
|
||||
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`;
|
||||
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId.toUpperCase()}`;
|
||||
export const STATE_DETAIL = "STATE_DETAILS";
|
||||
|
||||
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug}`;
|
||||
export const USER_ACTIVITY = (workspaceSlug: string) => `USER_ACTIVITY_${workspaceSlug}`;
|
||||
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
|
||||
export const USER_ACTIVITY = (workspaceSlug: string) => `USER_ACTIVITY_${workspaceSlug.toUpperCase()}`;
|
||||
export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
|
||||
`USER_WORKSPACE_DASHBOARD_${workspaceSlug}`;
|
||||
export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId}`;
|
||||
`USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`;
|
||||
export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId.toUpperCase()}`;
|
||||
|
||||
export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId}`;
|
||||
export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId}`;
|
||||
export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId.toUpperCase()}`;
|
||||
export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId.toUpperCase()}`;
|
||||
export const MODULE_ISSUES_WITH_PARAMS = (moduleId: string, params?: any) => {
|
||||
if (!params) return `MODULE_ISSUES_WITH_PARAMS_${moduleId}`;
|
||||
if (!params) return `MODULE_ISSUES_WITH_PARAMS_${moduleId.toUpperCase()}`;
|
||||
|
||||
const paramsKey = paramsToKey(params);
|
||||
|
||||
return `MODULE_ISSUES_WITH_PARAMS_${moduleId}_${paramsKey}`;
|
||||
return `MODULE_ISSUES_WITH_PARAMS_${moduleId}_${paramsKey.toUpperCase()}`;
|
||||
};
|
||||
export const MODULE_DETAILS = (moduleId: string) => `MODULE_DETAILS_${moduleId}`;
|
||||
export const MODULE_DETAILS = (moduleId: string) => `MODULE_DETAILS_${moduleId.toUpperCase()}`;
|
||||
|
||||
export const VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId}`;
|
||||
export const VIEW_ISSUES = (viewId: string) => `VIEW_ISSUES_${viewId}`;
|
||||
export const VIEW_DETAILS = (viewId: string) => `VIEW_DETAILS_${viewId}`;
|
||||
export const VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId.toUpperCase()}`;
|
||||
export const VIEW_ISSUES = (viewId: string) => `VIEW_ISSUES_${viewId.toUpperCase()}`;
|
||||
export const VIEW_DETAILS = (viewId: string) => `VIEW_DETAILS_${viewId.toUpperCase()}`;
|
||||
|
||||
// Issues
|
||||
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId}`;
|
||||
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId}`;
|
||||
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
|
||||
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
|
||||
|
||||
// integrations
|
||||
|
||||
// Pages
|
||||
export const RECENT_PAGES_LIST = (projectId: string) => `RECENT_PAGES_LIST_${projectId}`;
|
||||
export const ALL_PAGES_LIST = (projectId: string) => `ALL_PAGES_LIST_${projectId}`;
|
||||
export const FAVORITE_PAGES_LIST = (projectId: string) => `FAVORITE_PAGES_LIST_${projectId}`;
|
||||
export const MY_PAGES_LIST = (projectId: string) => `MY_PAGES_LIST_${projectId}`;
|
||||
export const OTHER_PAGES_LIST = (projectId: string) => `OTHER_PAGES_LIST_${projectId}`;
|
||||
export const PAGE_DETAILS = (pageId: string) => `PAGE_DETAILS_${pageId}`;
|
||||
export const PAGE_BLOCKS_LIST = (pageId: string) => `PAGE_BLOCK_LIST_${pageId}`;
|
||||
export const PAGE_BLOCK_DETAILS = (pageId: string) => `PAGE_BLOCK_DETAILS_${pageId}`;
|
||||
export const RECENT_PAGES_LIST = (projectId: string) => `RECENT_PAGES_LIST_${projectId.toUpperCase()}`;
|
||||
export const ALL_PAGES_LIST = (projectId: string) => `ALL_PAGES_LIST_${projectId.toUpperCase()}`;
|
||||
export const FAVORITE_PAGES_LIST = (projectId: string) => `FAVORITE_PAGES_LIST_${projectId.toUpperCase()}`;
|
||||
export const MY_PAGES_LIST = (projectId: string) => `MY_PAGES_LIST_${projectId.toUpperCase()}`;
|
||||
export const OTHER_PAGES_LIST = (projectId: string) => `OTHER_PAGES_LIST_${projectId.toUpperCase()}`;
|
||||
export const PAGE_DETAILS = (pageId: string) => `PAGE_DETAILS_${pageId.toUpperCase()}`;
|
||||
export const PAGE_BLOCKS_LIST = (pageId: string) => `PAGE_BLOCK_LIST_${pageId.toUpperCase()}`;
|
||||
export const PAGE_BLOCK_DETAILS = (pageId: string) => `PAGE_BLOCK_DETAILS_${pageId.toUpperCase()}`;
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@blueprintjs/popover2": "^1.13.3",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@jitsu/nextjs": "^3.1.5",
|
||||
"@remirror/core": "^2.0.11",
|
||||
"@remirror/extension-react-tables": "^2.2.11",
|
||||
"@remirror/pm": "^2.0.3",
|
||||
|
@ -42,10 +42,10 @@ const WorkspacePage: NextPage = () => {
|
||||
<div className="h-full w-full">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div
|
||||
className="flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg px-8 py-6 text-white md:flex-row md:items-center md:py-3"
|
||||
style={{ background: "linear-gradient(90deg, #8e2de2 0%, #4a00e0 100%)" }}
|
||||
className="flex flex-col bg-white justify-between gap-x-2 gap-y-6 rounded-lg px-8 py-6 text-black md:flex-row md:items-center md:py-3"
|
||||
// style={{ background: "linear-gradient(90deg, #8e2de2 0%, #4a00e0 100%)" }}
|
||||
>
|
||||
<p>Plane is a open source application, to support us you can star us on GitHub!</p>
|
||||
<p className="font-semibold">Plane is a open source application, to support us you can star us on GitHub!</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <a href="#" target="_blank" rel="noopener noreferrer">
|
||||
View roadmap
|
||||
@ -53,7 +53,7 @@ const WorkspacePage: NextPage = () => {
|
||||
<a
|
||||
href="https://github.com/makeplane/plane"
|
||||
target="_blank"
|
||||
className="rounded-md border-2 border-white px-3 py-1.5 text-sm duration-300 hover:bg-white hover:text-[#4a00e0]"
|
||||
className="rounded-md border-2 border-black font-medium px-3 py-1.5 text-sm duration-300"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Star us on GitHub
|
||||
|
@ -24,7 +24,7 @@ import AppLayout from "layouts/app-layout";
|
||||
import { SinglePageBlock } from "components/pages";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { CustomSearchSelect, Loader, PrimaryButton, TextArea } from "components/ui";
|
||||
import { CustomSearchSelect, Loader, PrimaryButton, TextArea, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { ArrowLeftIcon, PlusIcon, ShareIcon, StarIcon } from "@heroicons/react/24/outline";
|
||||
import { ColorPalletteIcon } from "components/icons";
|
||||
@ -324,9 +324,14 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-500">
|
||||
{renderShortTime(pageDetails.created_at)}
|
||||
<Tooltip
|
||||
tooltipContent={`Page last updated at ${renderShortTime(pageDetails.updated_at)}`}
|
||||
theme="dark"
|
||||
>
|
||||
<span className="cursor-default text-sm text-gray-500">
|
||||
{renderShortTime(pageDetails.updated_at)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<PrimaryButton className="flex items-center gap-2" onClick={handleCopyText}>
|
||||
<ShareIcon className="h-4 w-4" />
|
||||
Share
|
||||
@ -393,7 +398,7 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
onBlur={handleSubmit(updatePage)}
|
||||
onChange={(e) => setValue("name", e.target.value)}
|
||||
required={true}
|
||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-semibold outline-none ring-0 focus:ring-1 focus:ring-theme"
|
||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-semibold outline-none ring-0 focus:ring-1 focus:ring-gray-200"
|
||||
role="textbox"
|
||||
/>
|
||||
</div>
|
||||
@ -413,7 +418,7 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-2.5 py-1 text-xs hover:bg-gray-100"
|
||||
className="flex items-center gap-1 rounded bg-gray-100 px-2.5 py-1 text-xs hover:bg-gray-200"
|
||||
onClick={createPageBlock}
|
||||
disabled={isAddingBlock}
|
||||
>
|
||||
|
@ -120,12 +120,20 @@ const ProjectPages: NextPage<UserAuth> = (props) => {
|
||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||
mutate<IPage[]>(
|
||||
MY_PAGES_LIST(projectId as string),
|
||||
(prevData) => [res, ...(prevData as IPage[])],
|
||||
(prevData) => {
|
||||
if (!prevData) return undefined;
|
||||
|
||||
return [res, ...(prevData as IPage[])];
|
||||
},
|
||||
false
|
||||
);
|
||||
mutate<IPage[]>(
|
||||
ALL_PAGES_LIST(projectId as string),
|
||||
(prevData) => [res, ...(prevData as IPage[])],
|
||||
(prevData) => {
|
||||
if (!prevData) return undefined;
|
||||
|
||||
return [res, ...(prevData as IPage[])];
|
||||
},
|
||||
false
|
||||
);
|
||||
})
|
||||
|
50
apps/app/pages/api/track-event.ts
Normal file
50
apps/app/pages/api/track-event.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
// jitsu
|
||||
import { createClient } from "@jitsu/nextjs";
|
||||
import { convertCookieStringToObject } from "lib/cookie";
|
||||
|
||||
const jitsu = createClient({
|
||||
key: process.env.JITSU_ACCESS_KEY || "",
|
||||
tracking_host: "https://t.jitsu.com",
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { eventName, extra } = req.body;
|
||||
|
||||
if (!eventName) {
|
||||
return res.status(400).json({ message: "Bad request" });
|
||||
}
|
||||
|
||||
const cookie = convertCookieStringToObject(req.headers.cookie);
|
||||
const accessToken = cookie?.accessToken;
|
||||
|
||||
if (!accessToken) return res.status(401).json({ message: "Unauthorized" });
|
||||
|
||||
const user = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.user)
|
||||
.catch(() => res.status(401).json({ message: "Unauthorized" }));
|
||||
|
||||
if (!user) return res.status(401).json({ message: "Unauthorized" });
|
||||
|
||||
// TODO: cache user info
|
||||
|
||||
jitsu
|
||||
.id({
|
||||
...user,
|
||||
})
|
||||
.then(() => {
|
||||
jitsu.track(eventName, {
|
||||
...extra,
|
||||
});
|
||||
});
|
||||
|
||||
res.status(200).json({ message: "success" });
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@ -1,5 +1,7 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
// types
|
||||
import { IGptResponse } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
@ -12,7 +14,7 @@ class AiServices extends APIService {
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: { prompt: string; task: string }
|
||||
): Promise<any> {
|
||||
): Promise<IGptResponse> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
@ -1,5 +1,7 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
// types
|
||||
import type {
|
||||
CycleIssueResponse,
|
||||
@ -13,6 +15,9 @@ import type {
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class ProjectCycleServices extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -20,7 +25,10 @@ class ProjectCycleServices extends APIService {
|
||||
|
||||
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackCycleCreateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -86,7 +94,10 @@ class ProjectCycleServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackCycleUpdateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -102,7 +113,10 @@ class ProjectCycleServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackCycleUpdateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -110,7 +124,10 @@ class ProjectCycleServices extends APIService {
|
||||
|
||||
async deleteCycle(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackCycleDeleteEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -1,10 +1,14 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// type
|
||||
import type { IIssue, IIssueActivity, IIssueComment, IIssueViewOptions } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class ProjectIssuesServices extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -12,7 +16,10 @@ class ProjectIssuesServices extends APIService {
|
||||
|
||||
async createIssues(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackIssueCreateEvent(response.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -241,7 +248,10 @@ class ProjectIssuesServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackIssueUpdateEvent(data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -257,7 +267,10 @@ class ProjectIssuesServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackIssueUpdateEvent(data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -265,7 +278,10 @@ class ProjectIssuesServices extends APIService {
|
||||
|
||||
async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackIssueDeleteEvent({ issuesId });
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -276,7 +292,10 @@ class ProjectIssuesServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackIssueBulkDeleteEvent(data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -1,10 +1,15 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "./track-event.service";
|
||||
|
||||
// types
|
||||
import type { IIssueViewOptions, IModule, IIssue } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class ProjectIssuesServices extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -20,7 +25,10 @@ class ProjectIssuesServices extends APIService {
|
||||
|
||||
async createModule(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackModuleCreateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -36,7 +44,10 @@ class ProjectIssuesServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackModuleUpdateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -60,7 +71,10 @@ class ProjectIssuesServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackModuleUpdateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -70,7 +84,10 @@ class ProjectIssuesServices extends APIService {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackModuleDeleteEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -1,5 +1,7 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
// types
|
||||
import type {
|
||||
GithubRepositoriesResponse,
|
||||
@ -12,6 +14,9 @@ import type {
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class ProjectServices extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -19,7 +24,10 @@ class ProjectServices extends APIService {
|
||||
|
||||
async createProject(workspaceSlug: string, data: Partial<IProject>): Promise<IProject> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackCreateProjectEvent(response.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
@ -59,7 +67,10 @@ class ProjectServices extends APIService {
|
||||
data: Partial<IProject>
|
||||
): Promise<IProject> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackUpdateProjectEvent(response.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -67,7 +78,10 @@ class ProjectServices extends APIService {
|
||||
|
||||
async deleteProject(workspaceSlug: string, projectId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackDeleteProjectEvent({ projectId });
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -1,8 +1,12 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
// types
|
||||
import type { IState, StateResponse } from "types";
|
||||
|
||||
@ -13,7 +17,10 @@ class ProjectStateServices extends APIService {
|
||||
|
||||
async createState(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackStateCreateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -54,7 +61,10 @@ class ProjectStateServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackStateUpdateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -70,7 +80,10 @@ class ProjectStateServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackStateUpdateEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -78,7 +91,10 @@ class ProjectStateServices extends APIService {
|
||||
|
||||
async deleteState(workspaceSlug: string, projectId: string, stateId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackStateDeleteEvent(response?.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
314
apps/app/services/track-event.service.ts
Normal file
314
apps/app/services/track-event.service.ts
Normal file
@ -0,0 +1,314 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
// types
|
||||
import type { IWorkspace } from "types";
|
||||
|
||||
// TODO: as we add more events, we can refactor this to be divided into different classes
|
||||
class TrackEventServices extends APIService {
|
||||
constructor() {
|
||||
super("/");
|
||||
}
|
||||
|
||||
async trackCreateWorkspaceEvent(data: IWorkspace): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "CREATE_WORKSPACE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackUpdateWorkspaceEvent(data: IWorkspace): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "UPDATE_WORKSPACE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackDeleteWorkspaceEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "DELETE_WORKSPACE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackCreateProjectEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "CREATE_PROJECT",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackUpdateProjectEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "UPDATE_PROJECT",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackDeleteProjectEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "DELETE_PROJECT",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackWorkspaceUserInviteEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "WORKSPACE_USER_INVITE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackWorkspaceUserJoinEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "WORKSPACE_USER_INVITE_ACCEPT",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackWorkspaceUserBulkJoinEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "WORKSPACE_USER_BULK_INVITE_ACCEPT",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackUserOnboardingCompleteEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "USER_ONBOARDING_COMPLETE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackIssueCreateEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "ISSUE_CREATE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackIssueUpdateEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "ISSUE_UPDATE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackIssueDeleteEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "ISSUE_DELETE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackIssueBulkDeleteEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "ISSUE_BULK_DELETE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackStateCreateEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "STATE_CREATE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackStateUpdateEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "STATE_UPDATE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackStateDeleteEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "STATE_DELETE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackCycleCreateEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "CYCLE_CREATE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackCycleUpdateEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "CYCLE_UPDATE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackCycleDeleteEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "CYCLE_DELETE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackModuleCreateEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "MODULE_CREATE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackModuleUpdateEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "MODULE_UPDATE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackModuleDeleteEvent(data: any): Promise<any> {
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName: "MODULE_DELETE",
|
||||
extra: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const trackEventServices = new TrackEventServices();
|
||||
|
||||
export default trackEventServices;
|
@ -1,9 +1,14 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
import type { IUser, IUserActivity, IUserWorkspaceDashboard } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class UserService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -44,7 +49,10 @@ class UserService extends APIService {
|
||||
|
||||
async updateUserOnBoard(): Promise<any> {
|
||||
return this.patch("/api/users/me/onboard/", { is_onboarded: true })
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackUserOnboardingCompleteEvent(response.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
@ -14,6 +15,9 @@ import {
|
||||
IWorkspaceSearchResults,
|
||||
} from "types";
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class WorkspaceService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -37,7 +41,10 @@ class WorkspaceService extends APIService {
|
||||
|
||||
async createWorkspace(data: Partial<IWorkspace>): Promise<IWorkspace> {
|
||||
return this.post("/api/workspaces/", data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackCreateWorkspaceEvent(response.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -45,7 +52,10 @@ class WorkspaceService extends APIService {
|
||||
|
||||
async updateWorkspace(workspaceSlug: string, data: Partial<IWorkspace>): Promise<IWorkspace> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackUpdateWorkspaceEvent(response.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -53,7 +63,10 @@ class WorkspaceService extends APIService {
|
||||
|
||||
async deleteWorkspace(workspaceSlug: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/`)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackDeleteWorkspaceEvent({ workspaceSlug });
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -61,7 +74,10 @@ class WorkspaceService extends APIService {
|
||||
|
||||
async inviteWorkspace(workspaceSlug: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/invite/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackWorkspaceUserInviteEvent(response.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -75,7 +91,10 @@ class WorkspaceService extends APIService {
|
||||
headers: {},
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackWorkspaceUserJoinEvent(response.data);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
4
apps/app/types/ai.d.ts
vendored
Normal file
4
apps/app/types/ai.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export interface IGptResponse {
|
||||
response: string;
|
||||
response_html: string;
|
||||
}
|
1
apps/app/types/index.d.ts
vendored
1
apps/app/types/index.d.ts
vendored
@ -9,6 +9,7 @@ export * from "./modules";
|
||||
export * from "./views";
|
||||
export * from "./integration";
|
||||
export * from "./pages";
|
||||
export * from "./ai";
|
||||
|
||||
export type NestedKeyOf<ObjectType extends object> = {
|
||||
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
||||
|
@ -11,6 +11,8 @@
|
||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||
"NEXT_PUBLIC_ENABLE_OAUTH",
|
||||
"NEXT_PUBLIC_UNSPLASH_ACCESS",
|
||||
"NEXT_PUBLIC_TRACK_EVENTS",
|
||||
"JITSU_ACCESS_KEY",
|
||||
"NEXT_PUBLIC_CRISP_ID"
|
||||
],
|
||||
"pipeline": {
|
||||
|
Loading…
Reference in New Issue
Block a user