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,
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(

View File

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

View File

@ -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,
)

View File

@ -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(

View File

@ -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",

View File

@ -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>

View File

@ -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">
<div className="text-sm">
Content: <p className="text-gray-500">{content}</p>
</div>
{content && content !== "" && (
<div className="text-sm">
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"}`}>

View File

@ -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}

View File

@ -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));

View File

@ -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,16 +63,22 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
/>
{completedCycles ? (
completedCycles.completed_cycles.length > 0 ? (
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{completedCycles.completed_cycles.map((cycle) => (
<SingleCycleCard
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
isCompleted
/>
))}
<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
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
isCompleted
/>
))}
</div>
</div>
) : (
<EmptyState

View File

@ -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"

View File

@ -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">

View File

@ -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();

View File

@ -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",

View File

@ -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!",

View File

@ -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>

View File

@ -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) => (

View File

@ -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",

View File

@ -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={() => {

View File

@ -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"

View File

@ -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

View File

@ -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()}`;

View File

@ -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",

View File

@ -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

View File

@ -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)}
</span>
<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}
>

View File

@ -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
);
})

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
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) => {

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

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
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;
});

View File

@ -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
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 "./integration";
export * from "./pages";
export * from "./ai";
export type NestedKeyOf<ObjectType 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_OAUTH",
"NEXT_PUBLIC_UNSPLASH_ACCESS",
"NEXT_PUBLIC_TRACK_EVENTS",
"JITSU_ACCESS_KEY",
"NEXT_PUBLIC_CRISP_ID"
],
"pipeline": {

1455
yarn.lock

File diff suppressed because it is too large Load Diff