Merge branch 'develop' of github.com:makeplane/plane into develop

This commit is contained in:
pablohashescobar 2023-03-29 12:54:33 +05:30
commit f9fa345b25
41 changed files with 949 additions and 1701 deletions

View File

@ -93,6 +93,7 @@ from plane.api.views import (
CompletedCyclesEndpoint, CompletedCyclesEndpoint,
CycleFavoriteViewSet, CycleFavoriteViewSet,
DraftCyclesEndpoint, DraftCyclesEndpoint,
TransferCycleIssueEndpoint,
## End Cycles ## End Cycles
# Modules # Modules
ModuleViewSet, ModuleViewSet,
@ -629,6 +630,11 @@ urlpatterns = [
), ),
name="user-favorite-cycle", 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 ## End Cycles
# Issue # Issue
path( path(

View File

@ -51,6 +51,7 @@ from .cycle import (
CompletedCyclesEndpoint, CompletedCyclesEndpoint,
CycleFavoriteViewSet, CycleFavoriteViewSet,
DraftCyclesEndpoint, DraftCyclesEndpoint,
TransferCycleIssueEndpoint,
) )
from .asset import FileAssetEndpoint, UserAssetsEndpoint from .asset import FileAssetEndpoint, UserAssetsEndpoint
from .issue import ( from .issue import (

View File

@ -129,6 +129,36 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, 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): class CycleIssueViewSet(BaseViewSet):
serializer_class = CycleIssueSerializer serializer_class = CycleIssueSerializer
@ -230,6 +260,14 @@ class CycleIssueViewSet(BaseViewSet):
workspace__slug=slug, project_id=project_id, pk=cycle_id 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 # Get all CycleIssues already created
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues)) cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
records_to_update = [] records_to_update = []
@ -681,3 +719,60 @@ class CycleFavoriteViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, 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,
)

View File

@ -28,10 +28,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
prompt = request.data.get("prompt", False) prompt = request.data.get("prompt", False)
task = request.data.get("task", False) task = request.data.get("task", False)
if not prompt or not task: if not task:
return Response( return Response(
{"error": "Task and prompt are required"}, {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
final_text = task + "\n" + prompt final_text = task + "\n" + prompt
@ -45,7 +44,11 @@ class GPTIntegrationEndpoint(BaseAPIView):
) )
text = response.choices[0].text.strip() 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: except Exception as e:
capture_exception(e) capture_exception(e)
return Response( return Response(

View File

@ -26,7 +26,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q |= Q(**{f"{field}__icontains": query}) q |= Q(**{f"{field}__icontains": query})
return Workspace.objects.filter( return Workspace.objects.filter(
q, workspace_member__member=self.request.user q, workspace_member__member=self.request.user
).values("name", "id", "slug") ).distinct().values("name", "id", "slug")
def filter_projects(self, query, slug, project_id): def filter_projects(self, query, slug, project_id):
fields = ["name"] fields = ["name"]
@ -37,7 +37,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q, q,
Q(project_projectmember__member=self.request.user) | Q(network=2), Q(project_projectmember__member=self.request.user) | Q(network=2),
workspace__slug=slug, workspace__slug=slug,
).values("name", "id", "identifier", "workspace__slug") ).distinct().values("name", "id", "identifier", "workspace__slug")
def filter_issues(self, query, slug, project_id): def filter_issues(self, query, slug, project_id):
fields = ["name", "sequence_id"] fields = ["name", "sequence_id"]
@ -54,7 +54,7 @@ class GlobalSearchEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
).values( ).distinct().values(
"name", "name",
"id", "id",
"sequence_id", "sequence_id",
@ -73,7 +73,7 @@ class GlobalSearchEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
).values( ).distinct().values(
"name", "name",
"id", "id",
"project_id", "project_id",
@ -90,7 +90,7 @@ class GlobalSearchEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
).values( ).distinct().values(
"name", "name",
"id", "id",
"project_id", "project_id",
@ -107,7 +107,7 @@ class GlobalSearchEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
).values( ).distinct().values(
"name", "name",
"id", "id",
"project_id", "project_id",
@ -124,7 +124,7 @@ class GlobalSearchEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
).values( ).distinct().values(
"name", "name",
"id", "id",
"project_id", "project_id",

View File

@ -6,9 +6,9 @@ import useSWR, { mutate } from "swr";
import { import {
ArrowRightIcon, ArrowRightIcon,
ChartBarIcon, ChartBarIcon,
ChatBubbleOvalLeftEllipsisIcon,
ClipboardIcon, ClipboardIcon,
FolderPlusIcon, FolderPlusIcon,
InboxIcon,
MagnifyingGlassIcon, MagnifyingGlassIcon,
Squares2X2Icon, Squares2X2Icon,
TrashIcon, TrashIcon,
@ -27,7 +27,7 @@ import {
PeopleGroupIcon, PeopleGroupIcon,
SettingIcon, SettingIcon,
ViewListIcon, ViewListIcon,
PencilScribbleIcon PencilScribbleIcon,
} from "components/icons"; } from "components/icons";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
@ -37,6 +37,7 @@ import { Command } from "cmdk";
import useTheme from "hooks/use-theme"; import useTheme from "hooks/use-theme";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useDebounce from "hooks/use-debounce";
// components // components
import { import {
ShortcutsModal, ShortcutsModal,
@ -50,6 +51,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateModuleModal } from "components/modules"; import { CreateUpdateModuleModal } from "components/modules";
import { CreateProjectModal } from "components/project"; import { CreateProjectModal } from "components/project";
import { CreateUpdateViewModal } from "components/views"; import { CreateUpdateViewModal } from "components/views";
import { Spinner } from "components/ui";
// helpers // helpers
import { import {
capitalizeFirstLetter, capitalizeFirstLetter,
@ -58,12 +60,11 @@ import {
} from "helpers/string.helper"; } from "helpers/string.helper";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import workspaceService from "services/workspace.service";
// types // types
import { IIssue, IWorkspaceSearchResults } from "types"; import { IIssue, IWorkspaceSearchResults } from "types";
// fetch keys // fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/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 = () => { export const CommandPalette: React.FC = () => {
const [isPaletteOpen, setIsPaletteOpen] = useState(false); const [isPaletteOpen, setIsPaletteOpen] = useState(false);
@ -88,7 +89,9 @@ export const CommandPalette: React.FC = () => {
page: [], 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 debouncedSearchTerm = useDebounce(searchTerm, 500);
const [placeholder, setPlaceholder] = React.useState("Type a command or search..."); const [placeholder, setPlaceholder] = React.useState("Type a command or search...");
const [pages, setPages] = React.useState<string[]>([]); const [pages, setPages] = React.useState<string[]>([]);
@ -220,18 +223,39 @@ export const CommandPalette: React.FC = () => {
() => { () => {
if (!workspaceSlug || !projectId) return; 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. // or searchTerm has not been updated within last 500ms.
if (debouncedSearchTerm) { if (debouncedSearchTerm) {
setIsPendingAPIRequest(true); setIsSearching(true);
workspaceService workspaceService
.searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm) .searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm)
.then((results) => { .then((results) => {
setIsPendingAPIRequest(false);
setResults(results); 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 { } 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 [debouncedSearchTerm, workspaceSlug, projectId] // Only call effect if debounced search term changes
@ -369,11 +393,11 @@ export const CommandPalette: React.FC = () => {
}} }}
> >
{issueId && issueDetails && ( {issueId && issueDetails && (
<div className="p-3"> <div className="flex p-3">
<span className="rounded-md bg-slate-100 p-1 px-2 text-xs font-medium text-slate-500"> <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.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails?.name} {issueDetails?.name}
</span> </p>
</div> </div>
)} )}
<div className="relative"> <div className="relative">
@ -392,9 +416,20 @@ export const CommandPalette: React.FC = () => {
/> />
</div> </div>
<Command.List className="max-h-96 overflow-scroll p-2"> <Command.List className="max-h-96 overflow-scroll p-2">
<Command.Empty className="my-4 text-center text-gray-500"> {!isLoading &&
No results found. resultsCount === 0 &&
</Command.Empty> 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 !== "" && ( {debouncedSearchTerm !== "" && (
<> <>
@ -419,7 +454,8 @@ export const CommandPalette: React.FC = () => {
Icon = AssignmentClipboardIcon; Icon = AssignmentClipboardIcon;
} else if (key === "issue") { } else if (key === "issue") {
path = `/${item.workspace__slug}/projects/${item.project_id}/issues/${item.id}`; 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; Icon = LayerDiagonalIcon;
} else if (key === "issue_view") { } else if (key === "issue_view") {
path = `/${item.workspace__slug}/projects/${item.project_id}/views/${item.id}`; 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" className="focus:bg-slate-200 focus:outline-none"
tabIndex={0} 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" /> <Icon className="h-4 w-4" />
{item.name} <p className="block flex-1 truncate">{item.name}</p>
</div> </div>
</Command.Item> </Command.Item>
); );
@ -720,14 +756,14 @@ export const CommandPalette: React.FC = () => {
<Command.Item <Command.Item
onSelect={() => { onSelect={() => {
setIsPaletteOpen(false); 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" className="focus:bg-slate-200 focus:outline-none"
tabIndex={0} tabIndex={0}
> >
<div className="flex items-center gap-2 text-slate-700"> <div className="flex items-center gap-2 text-slate-700">
<InboxIcon className="h-4 w-4" /> <ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4" />
Email us Chat with us
</div> </div>
</Command.Item> </Command.Item>
</Command.Group> </Command.Group>

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import dynamic from "next/dynamic";
// react-hook-form // react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -16,6 +17,7 @@ type Props = {
handleClose: () => void; handleClose: () => void;
inset?: string; inset?: string;
content: string; content: string;
htmlContent?: string;
onResponse: (response: string) => void; onResponse: (response: string) => void;
}; };
@ -24,11 +26,16 @@ type FormData = {
task: string; task: string;
}; };
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
});
export const GptAssistantModal: React.FC<Props> = ({ export const GptAssistantModal: React.FC<Props> = ({
isOpen, isOpen,
handleClose, handleClose,
inset = "top-0 left-0", inset = "top-0 left-0",
content, content,
htmlContent,
onResponse, onResponse,
}) => { }) => {
const [response, setResponse] = useState(""); const [response, setResponse] = useState("");
@ -62,15 +69,6 @@ export const GptAssistantModal: React.FC<Props> = ({
const handleResponse = async (formData: FormData) => { const handleResponse = async (formData: FormData) => {
if (!workspaceSlug || !projectId) return; 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 === "") { if (formData.task === "") {
setToastAlert({ setToastAlert({
type: "error", type: "error",
@ -82,11 +80,11 @@ export const GptAssistantModal: React.FC<Props> = ({
await aiService await aiService
.createGptTask(workspaceSlug as string, projectId as string, { .createGptTask(workspaceSlug as string, projectId as string, {
prompt: content, prompt: content && content !== "" ? content : "",
task: formData.task, task: formData.task,
}) })
.then((res) => { .then((res) => {
setResponse(res.response); setResponse(res.response_html);
setFocus("task"); setFocus("task");
if (res.response === "") setInvalidResponse(true); if (res.response === "") setInvalidResponse(true);
@ -105,12 +103,28 @@ export const GptAssistantModal: React.FC<Props> = ({
}`} }`}
> >
<form onSubmit={handleSubmit(handleResponse)} className="space-y-4"> <form onSubmit={handleSubmit(handleResponse)} className="space-y-4">
{content && content !== "" && (
<div className="text-sm"> <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> </div>
)}
{response !== "" && ( {response !== "" && (
<div className="text-sm"> <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> </div>
)} )}
{invalidResponse && ( {invalidResponse && (
@ -123,7 +137,11 @@ export const GptAssistantModal: React.FC<Props> = ({
type="text" type="text"
name="task" name="task"
register={register} 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" autoComplete="off"
/> />
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}> <div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>

View File

@ -30,7 +30,7 @@ import {
TrashIcon, TrashIcon,
XMarkIcon, XMarkIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons"; import { ExclamationIcon, getStateGroupIcon } from "components/icons";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
// types // types
@ -683,6 +683,12 @@ export const IssuesView: React.FC<Props> = ({
{groupedByIssues ? ( {groupedByIssues ? (
isNotEmpty ? ( 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" ? ( {issueView === "list" ? (
<AllLists <AllLists
type={type} type={type}

View File

@ -13,12 +13,11 @@ import projectService from "services/project.service";
// hooks // hooks
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// components // components
import { LinksList, SingleProgressStats } from "components/core"; import { SingleProgressStats } from "components/core";
// ui // ui
import { Avatar } from "components/ui"; import { Avatar } from "components/ui";
// icons // icons
import User from "public/user.png"; import User from "public/user.png";
import { PlusIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssue, IIssueLabels, IModule, UserAuth } from "types"; import { IIssue, IIssueLabels, IModule, UserAuth } from "types";
// fetch-keys // fetch-keys
@ -28,8 +27,6 @@ type Props = {
groupedIssues: any; groupedIssues: any;
issues: IIssue[]; issues: IIssue[];
module?: IModule; module?: IModule;
setModuleLinkModal?: any;
handleDeleteLink?: any;
userAuth?: UserAuth; userAuth?: UserAuth;
}; };
@ -47,8 +44,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
groupedIssues, groupedIssues,
issues, issues,
module, module,
setModuleLinkModal,
handleDeleteLink,
userAuth, userAuth,
}) => { }) => {
const router = useRouter(); const router = useRouter();
@ -72,14 +67,12 @@ export const SidebarProgressStats: React.FC<Props> = ({
const currentValue = (tab: string | null) => { const currentValue = (tab: string | null) => {
switch (tab) { switch (tab) {
case "Links":
return 0;
case "Assignees": case "Assignees":
return 1; return 0;
case "Labels": case "Labels":
return 2; return 1;
case "States": case "States":
return 3; return 2;
default: default:
return 3; return 3;
@ -91,12 +84,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
onChange={(i) => { onChange={(i) => {
switch (i) { switch (i) {
case 0: case 0:
return setTab("Links");
case 1:
return setTab("Assignees"); return setTab("Assignees");
case 2: case 1:
return setTab("Labels"); return setTab("Labels");
case 3: case 2:
return setTab("States"); return setTab("States");
default: 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 className={`flex w-full items-center justify-between rounded-md bg-gray-100 px-1 py-1.5
${module ? "text-xs" : "text-sm"} `} ${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 <Tab
className={({ selected }) => className={({ selected }) =>
`w-full rounded px-3 py-1 text-gray-900 ${ `w-full rounded px-3 py-1 text-gray-900 ${
@ -152,29 +129,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
</Tab> </Tab>
</Tab.List> </Tab.List>
<Tab.Panels className="flex w-full items-center justify-between p-1"> <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 "> <Tab.Panel as="div" className="flex w-full flex-col text-xs ">
{members?.map((member, index) => { {members?.map((member, index) => {
const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id)); const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id));

View File

@ -9,7 +9,7 @@ import cyclesService from "services/cycles.service";
// components // components
import { DeleteCycleModal, SingleCycleCard } from "components/cycles"; import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
// icons // icons
import { CompletedCycleIcon } from "components/icons"; import { CompletedCycleIcon, ExclamationIcon } from "components/icons";
// types // types
import { ICycle, SelectCycleType } from "types"; import { ICycle, SelectCycleType } from "types";
// fetch-keys // fetch-keys
@ -63,6 +63,11 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
/> />
{completedCycles ? ( {completedCycles ? (
completedCycles.completed_cycles.length > 0 ? ( 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"> <div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{completedCycles.completed_cycles.map((cycle) => ( {completedCycles.completed_cycles.map((cycle) => (
<SingleCycleCard <SingleCycleCard
@ -74,6 +79,7 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
/> />
))} ))}
</div> </div>
</div>
) : ( ) : (
<EmptyState <EmptyState
type="cycle" type="cycle"

View File

@ -239,7 +239,11 @@ export const IssueForm: FC<IssueFormProps> = ({
control={control} control={control}
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<RemirrorRichTextEditor <RemirrorRichTextEditor
value={value} value={
!value || (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
onJSONChange={(jsonValue) => setValue("description", jsonValue)} onJSONChange={(jsonValue) => setValue("description", jsonValue)}
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
placeholder="Description" placeholder="Description"

View File

@ -14,6 +14,7 @@ import {
ChevronDownIcon, ChevronDownIcon,
DocumentDuplicateIcon, DocumentDuplicateIcon,
DocumentIcon, DocumentIcon,
PlusIcon,
TrashIcon, TrashIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
@ -24,7 +25,7 @@ import modulesService from "services/modules.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { LinkModal, SidebarProgressStats } from "components/core"; import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules"; import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules";
import ProgressChart from "components/core/sidebar/progress-chart"; import ProgressChart from "components/core/sidebar/progress-chart";
import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui"; import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui";
@ -414,7 +415,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
</div> </div>
{isStartValid && isEndValid ? ( {isStartValid && isEndValid ? (
<Disclosure.Button> <Disclosure.Button className="p-1">
<ChevronDownIcon <ChevronDownIcon
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
aria-hidden="true" aria-hidden="true"
@ -485,7 +486,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
</div> </div>
{issues.length > 0 ? ( {issues.length > 0 ? (
<Disclosure.Button> <Disclosure.Button className="p-1">
<ChevronDownIcon <ChevronDownIcon
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
aria-hidden="true" aria-hidden="true"
@ -508,8 +509,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
<SidebarProgressStats <SidebarProgressStats
issues={issues} issues={issues}
groupedIssues={groupedIssues} groupedIssues={groupedIssues}
setModuleLinkModal={setModuleLinkModal}
handleDeleteLink={handleDeleteLink}
userAuth={userAuth} userAuth={userAuth}
module={module} module={module}
/> />
@ -524,6 +523,27 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
)} )}
</Disclosure> </Disclosure>
</div> </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"> <Loader className="px-5">

View File

@ -45,12 +45,20 @@ export const CreateUpdatePageModal: React.FC<Props> = ({ isOpen, handleClose, da
mutate(RECENT_PAGES_LIST(projectId as string)); mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>( mutate<IPage[]>(
MY_PAGES_LIST(projectId as string), MY_PAGES_LIST(projectId as string),
(prevData) => [res, ...(prevData as IPage[])], (prevData) => {
if (!prevData) return undefined;
return [res, ...(prevData as IPage[])];
},
false false
); );
mutate<IPage[]>( mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string), ALL_PAGES_LIST(projectId as string),
(prevData) => [res, ...(prevData as IPage[])], (prevData) => {
if (!prevData) return undefined;
return [res, ...(prevData as IPage[])];
},
false false
); );
onClose(); onClose();

View File

@ -2,6 +2,8 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
@ -14,6 +16,13 @@ import { DangerButton, SecondaryButton } from "components/ui";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// types // types
import type { IPage } from "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 = { type TConfirmPageDeletionProps = {
isOpen: boolean; isOpen: boolean;
@ -45,6 +54,22 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = ({
await pagesService await pagesService
.deletePage(workspaceSlug as string, data.project, data.id) .deletePage(workspaceSlug as string, data.project, data.id)
.then(() => { .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(); handleClose();
setToastAlert({ setToastAlert({
type: "success", type: "success",

View File

@ -57,7 +57,6 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
const handleAddToFavorites = (page: IPage) => { const handleAddToFavorites = (page: IPage) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>( mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string), ALL_PAGES_LIST(projectId as string),
(prevData) => (prevData) =>
@ -89,6 +88,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
page: page.id, page: page.id,
}) })
.then(() => { .then(() => {
mutate(RECENT_PAGES_LIST(projectId as string));
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -107,7 +107,6 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
const handleRemoveFromFavorites = (page: IPage) => { const handleRemoveFromFavorites = (page: IPage) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>( mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string), ALL_PAGES_LIST(projectId as string),
(prevData) => (prevData) =>
@ -137,6 +136,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
pagesService pagesService
.removePageFromFavorites(workspaceSlug as string, projectId as string, page.id) .removePageFromFavorites(workspaceSlug as string, projectId as string, page.id)
.then(() => { .then(() => {
mutate(RECENT_PAGES_LIST(projectId as string));
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",

View File

@ -17,11 +17,16 @@ import useToast from "hooks/use-toast";
import { CreateUpdateIssueModal } from "components/issues"; import { CreateUpdateIssueModal } from "components/issues";
import { GptAssistantModal } from "components/core"; import { GptAssistantModal } from "components/core";
// ui // ui
import { CustomMenu, Loader, TextArea } from "components/ui"; import { CustomMenu, Input, Loader, TextArea } from "components/ui";
// icons // icons
import { LayerDiagonalIcon, WaterDropIcon } from "components/icons"; import { LayerDiagonalIcon } from "components/icons";
import { ArrowPathIcon } from "@heroicons/react/20/solid"; 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 // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
@ -163,21 +168,8 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
const handleAiAssistance = async (response: string) => { const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
setValue("description", { setValue("description", {});
type: "doc", setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
content: [
{
type: "paragraph",
content: [
{
text: response,
type: "text",
},
],
},
],
});
setValue("description_html", `<p>${response}</p>`);
handleSubmit(updatePageBlock)() handleSubmit(updatePageBlock)()
.then(() => { .then(() => {
setToastAlert({ 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"> <div className="-mx-3 -mt-2 flex items-center justify-between gap-2">
<TextArea <Input
id="name" id="name"
name="name" name="name"
placeholder="Block title" placeholder="Block title"
@ -261,11 +253,11 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
onBlur={handleSubmit(updatePageBlock)} onBlur={handleSubmit(updatePageBlock)}
onChange={(e) => setValue("name", e.target.value)} onChange={(e) => setValue("name", e.target.value)}
required={true} 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" role="textbox"
/> />
<div className="flex flex-shrink-0 items-center gap-2"> <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"> <div className="flex flex-shrink-0 cursor-default items-center gap-1 rounded bg-gray-100 py-1 px-1.5 text-xs">
{isSyncing ? ( {isSyncing ? (
<ArrowPathIcon className="h-3 w-3 animate-spin" /> <ArrowPathIcon className="h-3 w-3 animate-spin" />
@ -285,12 +277,13 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
)} )}
<button <button
type="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)} onClick={() => setGptAssistantModal((prevData) => !prevData)}
> >
<SparklesIcon className="h-4 w-4" />
AI AI
</button> </button>
<CustomMenu label={<WaterDropIcon width={14} height={15} />} noBorder noChevron> <CustomMenu label={<BoltIcon className="h-4.5 w-3.5" />} noBorder noChevron>
{block.issue ? ( {block.issue ? (
<> <>
<CustomMenu.MenuItem onClick={handleBlockSync}> <CustomMenu.MenuItem onClick={handleBlockSync}>
@ -312,7 +305,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
</CustomMenu> </CustomMenu>
</div> </div>
</div> </div>
<div className="page-block-section relative -mx-3 -mt-5"> <div className="page-block-section font relative -mx-3 -mt-3">
<Controller <Controller
name="description" name="description"
control={control} control={control}
@ -327,8 +320,9 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
onJSONChange={(jsonValue) => setValue("description", jsonValue)} onJSONChange={(jsonValue) => setValue("description", jsonValue)}
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
placeholder="Block description..." placeholder="Block description..."
customClassName="text-gray-500" customClassName="border border-transparent"
noBorder noBorder
borderOnFocus
/> />
)} )}
/> />
@ -337,6 +331,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
handleClose={() => setGptAssistantModal(false)} handleClose={() => setGptAssistantModal(false)}
inset="top-2 left-0" inset="top-2 left-0"
content={block.description_stripped} content={block.description_stripped}
htmlContent={block.description_html}
onResponse={handleAiAssistance} onResponse={handleAiAssistance}
/> />
</div> </div>

View File

@ -6,13 +6,12 @@ import { useRouter } from "next/router";
// ui // ui
import { CustomMenu, Tooltip } from "components/ui"; import { CustomMenu, Tooltip } from "components/ui";
// icons // icons
import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline"; import { DocumentTextIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { renderShortDate, renderShortTime } from "helpers/date-time.helper"; import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
// types // types
import { IPage } from "types"; import { IPage } from "types";
import { PencilScribbleIcon } from "components/icons";
type TSingleStatProps = { type TSingleStatProps = {
page: IPage; page: IPage;
@ -39,7 +38,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
<div className="relative rounded p-4 hover:bg-gray-100"> <div className="relative rounded p-4 hover:bg-gray-100">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <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> <p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
{page.label_details.length > 0 && {page.label_details.length > 0 &&
page.label_details.map((label) => ( page.label_details.map((label) => (

View File

@ -6,7 +6,7 @@ import { Disclosure, Transition } from "@headlessui/react";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// icons // icons
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
import { import {
ContrastIcon, ContrastIcon,
LayerDiagonalIcon, LayerDiagonalIcon,
@ -53,7 +53,7 @@ const navigation = (workspaceSlug: string, projectId: string) => [
{ {
name: "Pages", name: "Pages",
href: `/${workspaceSlug}/projects/${projectId}/pages`, href: `/${workspaceSlug}/projects/${projectId}/pages`,
icon: PencilScribbleIcon, icon: DocumentTextIcon,
}, },
{ {
name: "Settings", name: "Settings",

View File

@ -50,6 +50,7 @@ export interface IRemirrorRichTextEditor {
customClassName?: string; customClassName?: string;
gptOption?: boolean; gptOption?: boolean;
noBorder?: boolean; noBorder?: boolean;
borderOnFocus?: boolean;
} }
// eslint-disable-next-line no-duplicate-imports // eslint-disable-next-line no-duplicate-imports
@ -69,6 +70,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
customClassName, customClassName,
gptOption = false, gptOption = false,
noBorder = false, noBorder = false,
borderOnFocus = true,
} = props; } = props;
const [imageLoader, setImageLoader] = useState(false); const [imageLoader, setImageLoader] = useState(false);
@ -188,9 +190,9 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
manager={manager} manager={manager}
initialContent={state} initialContent={state}
classNames={[ 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" noBorder ? "" : "border"
} ${customClassName}`, } ${borderOnFocus ? "focus:border" : ""} ${customClassName}`,
]} ]}
editable={editable} editable={editable}
onBlur={() => { onBlur={() => {

View File

@ -60,7 +60,7 @@ export const CompletedIssuesGraph: React.FC<Props> = ({ month, issues, setMonth
<LineChart data={data}> <LineChart data={data}>
<CartesianGrid stroke="#e2e2e2" /> <CartesianGrid stroke="#e2e2e2" />
<XAxis dataKey="week_in_month" /> <XAxis dataKey="week_in_month" />
<YAxis dataKey="completed_count" /> <YAxis dataKey="completed_count" allowDecimals={false} />
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip />} />
<Line <Line
type="monotone" type="monotone"

View File

@ -8,14 +8,12 @@ import { Transition } from "@headlessui/react";
import useTheme from "hooks/use-theme"; import useTheme from "hooks/use-theme";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// icons // icons
import { ArrowLongLeftIcon, ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
import { import {
QuestionMarkCircleIcon, ArrowLongLeftIcon,
BoltIcon, ChatBubbleOvalLeftEllipsisIcon,
DocumentIcon, RocketLaunchIcon,
DiscordIcon, } from "@heroicons/react/24/outline";
GithubIcon, import { QuestionMarkCircleIcon, DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
} from "components/icons";
const helpOptions = [ const helpOptions = [
{ {
@ -77,7 +75,7 @@ export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
}} }}
title="Shortcuts" title="Shortcuts"
> >
<BoltIcon className="h-4 w-4 text-gray-500" /> <RocketLaunchIcon className="h-4 w-4 text-gray-500" />
{!sidebarCollapse && <span>Shortcuts</span>} {!sidebarCollapse && <span>Shortcuts</span>}
</button> </button>
<button <button

View File

@ -23,95 +23,95 @@ export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
export const USER_WORKSPACES = "USER_WORKSPACES"; export const USER_WORKSPACES = "USER_WORKSPACES";
export const APP_INTEGRATIONS = "APP_INTEGRATIONS"; 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) => 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) => 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_INVITATIONS = "WORKSPACE_INVITATIONS";
export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION"; export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION";
export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "LAST_ACTIVE_WORKSPACE_AND_PROJECTS"; 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) => export const FAVORITE_PROJECTS_LIST = (workspaceSlug: string) =>
`FAVORITE_PROJECTS_LIST_${workspaceSlug}`; `FAVORITE_PROJECTS_LIST_${workspaceSlug.toUpperCase()}`;
export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId}`; 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_INVITATIONS = "PROJECT_INVITATIONS";
export const PROJECT_ISSUES_LIST = (workspaceSlug: string, projectId: string) => 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) => { 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); 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) => export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
`PROJECT_ISSUES_PROPERTIES_${projectId}`; `PROJECT_ISSUES_PROPERTIES_${projectId.toUpperCase()}`;
export const PROJECT_ISSUES_COMMENTS = (issueId: string) => `PROJECT_ISSUES_COMMENTS_${issueId}`; export const PROJECT_ISSUES_COMMENTS = (issueId: string) => `PROJECT_ISSUES_COMMENTS_${issueId.toUpperCase()}`;
export const PROJECT_ISSUES_ACTIVITY = (issueId: string) => `PROJECT_ISSUES_ACTIVITY_${issueId}`; 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}`; 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}`; export const PROJECT_ISSUE_LABELS = (projectId: string) => `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`;
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => 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_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`;
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`; export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId.toUpperCase()}`;
export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { 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); 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) => export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) =>
`CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId}`; `CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId.toUpperCase()}`;
export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId}`; export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId.toUpperCase()}`;
export const CYCLE_COMPLETE_LIST = (projectId: string) => `CYCLE_COMPLETE_LIST_${projectId}`; 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 STATE_DETAIL = "STATE_DETAILS";
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug}`; export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
export const USER_ACTIVITY = (workspaceSlug: string) => `USER_ACTIVITY_${workspaceSlug}`; export const USER_ACTIVITY = (workspaceSlug: string) => `USER_ACTIVITY_${workspaceSlug.toUpperCase()}`;
export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) => export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
`USER_WORKSPACE_DASHBOARD_${workspaceSlug}`; `USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`;
export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId}`; export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId.toUpperCase()}`;
export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId}`; export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId.toUpperCase()}`;
export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId}`; export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId.toUpperCase()}`;
export const MODULE_ISSUES_WITH_PARAMS = (moduleId: string, params?: any) => { 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); 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 VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId.toUpperCase()}`;
export const VIEW_ISSUES = (viewId: string) => `VIEW_ISSUES_${viewId}`; export const VIEW_ISSUES = (viewId: string) => `VIEW_ISSUES_${viewId.toUpperCase()}`;
export const VIEW_DETAILS = (viewId: string) => `VIEW_DETAILS_${viewId}`; export const VIEW_DETAILS = (viewId: string) => `VIEW_DETAILS_${viewId.toUpperCase()}`;
// Issues // Issues
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId}`; export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId}`; export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
// integrations // integrations
// Pages // Pages
export const RECENT_PAGES_LIST = (projectId: string) => `RECENT_PAGES_LIST_${projectId}`; export const RECENT_PAGES_LIST = (projectId: string) => `RECENT_PAGES_LIST_${projectId.toUpperCase()}`;
export const ALL_PAGES_LIST = (projectId: string) => `ALL_PAGES_LIST_${projectId}`; export const ALL_PAGES_LIST = (projectId: string) => `ALL_PAGES_LIST_${projectId.toUpperCase()}`;
export const FAVORITE_PAGES_LIST = (projectId: string) => `FAVORITE_PAGES_LIST_${projectId}`; export const FAVORITE_PAGES_LIST = (projectId: string) => `FAVORITE_PAGES_LIST_${projectId.toUpperCase()}`;
export const MY_PAGES_LIST = (projectId: string) => `MY_PAGES_LIST_${projectId}`; export const MY_PAGES_LIST = (projectId: string) => `MY_PAGES_LIST_${projectId.toUpperCase()}`;
export const OTHER_PAGES_LIST = (projectId: string) => `OTHER_PAGES_LIST_${projectId}`; export const OTHER_PAGES_LIST = (projectId: string) => `OTHER_PAGES_LIST_${projectId.toUpperCase()}`;
export const PAGE_DETAILS = (pageId: string) => `PAGE_DETAILS_${pageId}`; export const PAGE_DETAILS = (pageId: string) => `PAGE_DETAILS_${pageId.toUpperCase()}`;
export const PAGE_BLOCKS_LIST = (pageId: string) => `PAGE_BLOCK_LIST_${pageId}`; export const PAGE_BLOCKS_LIST = (pageId: string) => `PAGE_BLOCK_LIST_${pageId.toUpperCase()}`;
export const PAGE_BLOCK_DETAILS = (pageId: string) => `PAGE_BLOCK_DETAILS_${pageId}`; export const PAGE_BLOCK_DETAILS = (pageId: string) => `PAGE_BLOCK_DETAILS_${pageId.toUpperCase()}`;

View File

@ -13,6 +13,7 @@
"@blueprintjs/popover2": "^1.13.3", "@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.3", "@headlessui/react": "^1.7.3",
"@heroicons/react": "^2.0.12", "@heroicons/react": "^2.0.12",
"@jitsu/nextjs": "^3.1.5",
"@remirror/core": "^2.0.11", "@remirror/core": "^2.0.11",
"@remirror/extension-react-tables": "^2.2.11", "@remirror/extension-react-tables": "^2.2.11",
"@remirror/pm": "^2.0.3", "@remirror/pm": "^2.0.3",

View File

@ -42,10 +42,10 @@ const WorkspacePage: NextPage = () => {
<div className="h-full w-full"> <div className="h-full w-full">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div <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" 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%)" }} // 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"> <div className="flex items-center gap-2">
{/* <a href="#" target="_blank" rel="noopener noreferrer"> {/* <a href="#" target="_blank" rel="noopener noreferrer">
View roadmap View roadmap
@ -53,7 +53,7 @@ const WorkspacePage: NextPage = () => {
<a <a
href="https://github.com/makeplane/plane" href="https://github.com/makeplane/plane"
target="_blank" 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" rel="noopener noreferrer"
> >
Star us on GitHub Star us on GitHub

View File

@ -24,7 +24,7 @@ import AppLayout from "layouts/app-layout";
import { SinglePageBlock } from "components/pages"; import { SinglePageBlock } from "components/pages";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { CustomSearchSelect, Loader, PrimaryButton, TextArea } from "components/ui"; import { CustomSearchSelect, Loader, PrimaryButton, TextArea, Tooltip } from "components/ui";
// icons // icons
import { ArrowLeftIcon, PlusIcon, ShareIcon, StarIcon } from "@heroicons/react/24/outline"; import { ArrowLeftIcon, PlusIcon, ShareIcon, StarIcon } from "@heroicons/react/24/outline";
import { ColorPalletteIcon } from "components/icons"; import { ColorPalletteIcon } from "components/icons";
@ -324,9 +324,14 @@ const SinglePage: NextPage<UserAuth> = (props) => {
)} )}
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm text-gray-500"> <Tooltip
{renderShortTime(pageDetails.created_at)} 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> </span>
</Tooltip>
<PrimaryButton className="flex items-center gap-2" onClick={handleCopyText}> <PrimaryButton className="flex items-center gap-2" onClick={handleCopyText}>
<ShareIcon className="h-4 w-4" /> <ShareIcon className="h-4 w-4" />
Share Share
@ -393,7 +398,7 @@ const SinglePage: NextPage<UserAuth> = (props) => {
onBlur={handleSubmit(updatePage)} onBlur={handleSubmit(updatePage)}
onChange={(e) => setValue("name", e.target.value)} onChange={(e) => setValue("name", e.target.value)}
required={true} 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" role="textbox"
/> />
</div> </div>
@ -413,7 +418,7 @@ const SinglePage: NextPage<UserAuth> = (props) => {
)} )}
<button <button
type="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} onClick={createPageBlock}
disabled={isAddingBlock} disabled={isAddingBlock}
> >

View File

@ -120,12 +120,20 @@ const ProjectPages: NextPage<UserAuth> = (props) => {
mutate(RECENT_PAGES_LIST(projectId as string)); mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>( mutate<IPage[]>(
MY_PAGES_LIST(projectId as string), MY_PAGES_LIST(projectId as string),
(prevData) => [res, ...(prevData as IPage[])], (prevData) => {
if (!prevData) return undefined;
return [res, ...(prevData as IPage[])];
},
false false
); );
mutate<IPage[]>( mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string), ALL_PAGES_LIST(projectId as string),
(prevData) => [res, ...(prevData as IPage[])], (prevData) => {
if (!prevData) return undefined;
return [res, ...(prevData as IPage[])];
},
false false
); );
}) })

View 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

View File

@ -1,5 +1,7 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
// types
import { IGptResponse } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@ -12,7 +14,7 @@ class AiServices extends APIService {
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
data: { prompt: string; task: string } data: { prompt: string; task: string }
): Promise<any> { ): Promise<IGptResponse> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {

View File

@ -1,5 +1,7 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
// types // types
import type { import type {
CycleIssueResponse, CycleIssueResponse,
@ -13,6 +15,9 @@ import type {
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 { class ProjectCycleServices extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); 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> { async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -86,7 +94,10 @@ class ProjectCycleServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackCycleUpdateEvent(response?.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -102,7 +113,10 @@ class ProjectCycleServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackCycleUpdateEvent(response?.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -110,7 +124,10 @@ class ProjectCycleServices extends APIService {
async deleteCycle(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> { async deleteCycle(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -1,10 +1,14 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
// type // type
import type { IIssue, IIssueActivity, IIssueComment, IIssueViewOptions } from "types"; import type { IIssue, IIssueActivity, IIssueComment, IIssueViewOptions } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 { class ProjectIssuesServices extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); 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> { async createIssues(workspaceSlug: string, projectId: string, data: any): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -241,7 +248,10 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackIssueUpdateEvent(data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -257,7 +267,10 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackIssueUpdateEvent(data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -265,7 +278,10 @@ class ProjectIssuesServices extends APIService {
async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise<any> { async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -276,7 +292,10 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackIssueBulkDeleteEvent(data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -1,10 +1,15 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "./track-event.service";
// types // types
import type { IIssueViewOptions, IModule, IIssue } from "types"; import type { IIssueViewOptions, IModule, IIssue } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 { class ProjectIssuesServices extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); 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> { async createModule(workspaceSlug: string, projectId: string, data: any): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -36,7 +44,10 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackModuleUpdateEvent(response?.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -60,7 +71,10 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackModuleUpdateEvent(response?.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -70,7 +84,10 @@ class ProjectIssuesServices extends APIService {
return this.delete( return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/` `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackModuleDeleteEvent(response?.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -1,5 +1,7 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
// types // types
import type { import type {
GithubRepositoriesResponse, GithubRepositoriesResponse,
@ -12,6 +14,9 @@ import type {
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 { class ProjectServices extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); 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> { async createProject(workspaceSlug: string, data: Partial<IProject>): Promise<IProject> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data) 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) => { .catch((error) => {
throw error?.response; throw error?.response;
}); });
@ -59,7 +67,10 @@ class ProjectServices extends APIService {
data: Partial<IProject> data: Partial<IProject>
): Promise<IProject> { ): Promise<IProject> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -67,7 +78,10 @@ class ProjectServices extends APIService {
async deleteProject(workspaceSlug: string, projectId: string): Promise<any> { async deleteProject(workspaceSlug: string, projectId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackDeleteProjectEvent({ projectId });
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -1,8 +1,12 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 // types
import type { IState, StateResponse } from "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> { async createState(workspaceSlug: string, projectId: string, data: any): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -54,7 +61,10 @@ class ProjectStateServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackStateUpdateEvent(response?.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -70,7 +80,10 @@ class ProjectStateServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackStateUpdateEvent(response?.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -78,7 +91,10 @@ class ProjectStateServices extends APIService {
async deleteState(workspaceSlug: string, projectId: string, stateId: string): Promise<any> { async deleteState(workspaceSlug: string, projectId: string, stateId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View 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;

View File

@ -1,9 +1,14 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
import type { IUser, IUserActivity, IUserWorkspaceDashboard } from "types"; import type { IUser, IUserActivity, IUserWorkspaceDashboard } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 { class UserService extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
@ -44,7 +49,10 @@ class UserService extends APIService {
async updateUserOnBoard(): Promise<any> { async updateUserOnBoard(): Promise<any> {
return this.patch("/api/users/me/onboard/", { is_onboarded: true }) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -1,5 +1,6 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@ -14,6 +15,9 @@ import {
IWorkspaceSearchResults, IWorkspaceSearchResults,
} from "types"; } from "types";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class WorkspaceService extends APIService { class WorkspaceService extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
@ -37,7 +41,10 @@ class WorkspaceService extends APIService {
async createWorkspace(data: Partial<IWorkspace>): Promise<IWorkspace> { async createWorkspace(data: Partial<IWorkspace>): Promise<IWorkspace> {
return this.post("/api/workspaces/", data) return this.post("/api/workspaces/", data)
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackCreateWorkspaceEvent(response.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -45,7 +52,10 @@ class WorkspaceService extends APIService {
async updateWorkspace(workspaceSlug: string, data: Partial<IWorkspace>): Promise<IWorkspace> { async updateWorkspace(workspaceSlug: string, data: Partial<IWorkspace>): Promise<IWorkspace> {
return this.patch(`/api/workspaces/${workspaceSlug}/`, data) return this.patch(`/api/workspaces/${workspaceSlug}/`, data)
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackUpdateWorkspaceEvent(response.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -53,7 +63,10 @@ class WorkspaceService extends APIService {
async deleteWorkspace(workspaceSlug: string): Promise<any> { async deleteWorkspace(workspaceSlug: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/`) return this.delete(`/api/workspaces/${workspaceSlug}/`)
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackDeleteWorkspaceEvent({ workspaceSlug });
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -61,7 +74,10 @@ class WorkspaceService extends APIService {
async inviteWorkspace(workspaceSlug: string, data: any): Promise<any> { async inviteWorkspace(workspaceSlug: string, data: any): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/invite/`, data) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -75,7 +91,10 @@ class WorkspaceService extends APIService {
headers: {}, headers: {},
} }
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent) trackEventServices.trackWorkspaceUserJoinEvent(response.data);
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

4
apps/app/types/ai.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export interface IGptResponse {
response: string;
response_html: string;
}

View File

@ -9,6 +9,7 @@ export * from "./modules";
export * from "./views"; export * from "./views";
export * from "./integration"; export * from "./integration";
export * from "./pages"; export * from "./pages";
export * from "./ai";
export type NestedKeyOf<ObjectType extends object> = { export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object

View File

@ -11,6 +11,8 @@
"NEXT_PUBLIC_ENABLE_SENTRY", "NEXT_PUBLIC_ENABLE_SENTRY",
"NEXT_PUBLIC_ENABLE_OAUTH", "NEXT_PUBLIC_ENABLE_OAUTH",
"NEXT_PUBLIC_UNSPLASH_ACCESS", "NEXT_PUBLIC_UNSPLASH_ACCESS",
"NEXT_PUBLIC_TRACK_EVENTS",
"JITSU_ACCESS_KEY",
"NEXT_PUBLIC_CRISP_ID" "NEXT_PUBLIC_CRISP_ID"
], ],
"pipeline": { "pipeline": {

1455
yarn.lock

File diff suppressed because it is too large Load Diff