forked from github/plane
Merge branch 'develop' of github.com:makeplane/plane into develop
This commit is contained in:
commit
f9fa345b25
@ -93,6 +93,7 @@ from plane.api.views import (
|
|||||||
CompletedCyclesEndpoint,
|
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(
|
||||||
|
@ -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 (
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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(
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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"}`}>
|
||||||
|
@ -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}
|
||||||
|
@ -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));
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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">
|
||||||
|
@ -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();
|
||||||
|
@ -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",
|
||||||
|
@ -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!",
|
||||||
|
@ -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>
|
||||||
|
@ -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) => (
|
||||||
|
@ -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",
|
||||||
|
@ -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={() => {
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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()}`;
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
50
apps/app/pages/api/track-event.ts
Normal file
50
apps/app/pages/api/track-event.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
// jitsu
|
||||||
|
import { createClient } from "@jitsu/nextjs";
|
||||||
|
import { convertCookieStringToObject } from "lib/cookie";
|
||||||
|
|
||||||
|
const jitsu = createClient({
|
||||||
|
key: process.env.JITSU_ACCESS_KEY || "",
|
||||||
|
tracking_host: "https://t.jitsu.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { eventName, extra } = req.body;
|
||||||
|
|
||||||
|
if (!eventName) {
|
||||||
|
return res.status(400).json({ message: "Bad request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookie = convertCookieStringToObject(req.headers.cookie);
|
||||||
|
const accessToken = cookie?.accessToken;
|
||||||
|
|
||||||
|
if (!accessToken) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const user = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.user)
|
||||||
|
.catch(() => res.status(401).json({ message: "Unauthorized" }));
|
||||||
|
|
||||||
|
if (!user) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
// TODO: cache user info
|
||||||
|
|
||||||
|
jitsu
|
||||||
|
.id({
|
||||||
|
...user,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
jitsu.track(eventName, {
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({ message: "success" });
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@ -1,5 +1,7 @@
|
|||||||
// services
|
// 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) => {
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
314
apps/app/services/track-event.service.ts
Normal file
314
apps/app/services/track-event.service.ts
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
// services
|
||||||
|
import APIService from "services/api.service";
|
||||||
|
// types
|
||||||
|
import type { IWorkspace } from "types";
|
||||||
|
|
||||||
|
// TODO: as we add more events, we can refactor this to be divided into different classes
|
||||||
|
class TrackEventServices extends APIService {
|
||||||
|
constructor() {
|
||||||
|
super("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackCreateWorkspaceEvent(data: IWorkspace): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "CREATE_WORKSPACE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackUpdateWorkspaceEvent(data: IWorkspace): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "UPDATE_WORKSPACE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackDeleteWorkspaceEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "DELETE_WORKSPACE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackCreateProjectEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "CREATE_PROJECT",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackUpdateProjectEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "UPDATE_PROJECT",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackDeleteProjectEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "DELETE_PROJECT",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackWorkspaceUserInviteEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "WORKSPACE_USER_INVITE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackWorkspaceUserJoinEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "WORKSPACE_USER_INVITE_ACCEPT",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackWorkspaceUserBulkJoinEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "WORKSPACE_USER_BULK_INVITE_ACCEPT",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackUserOnboardingCompleteEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "USER_ONBOARDING_COMPLETE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackIssueCreateEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "ISSUE_CREATE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackIssueUpdateEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "ISSUE_UPDATE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackIssueDeleteEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "ISSUE_DELETE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackIssueBulkDeleteEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "ISSUE_BULK_DELETE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackStateCreateEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "STATE_CREATE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackStateUpdateEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "STATE_UPDATE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackStateDeleteEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "STATE_DELETE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackCycleCreateEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "CYCLE_CREATE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackCycleUpdateEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "CYCLE_UPDATE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackCycleDeleteEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "CYCLE_DELETE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackModuleCreateEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "MODULE_CREATE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackModuleUpdateEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "MODULE_UPDATE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackModuleDeleteEvent(data: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName: "MODULE_DELETE",
|
||||||
|
extra: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackEventServices = new TrackEventServices();
|
||||||
|
|
||||||
|
export default trackEventServices;
|
@ -1,9 +1,14 @@
|
|||||||
// services
|
// 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;
|
||||||
});
|
});
|
||||||
|
@ -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
4
apps/app/types/ai.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface IGptResponse {
|
||||||
|
response: string;
|
||||||
|
response_html: string;
|
||||||
|
}
|
1
apps/app/types/index.d.ts
vendored
1
apps/app/types/index.d.ts
vendored
@ -9,6 +9,7 @@ export * from "./modules";
|
|||||||
export * from "./views";
|
export * from "./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
|
||||||
|
@ -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": {
|
||||||
|
Loading…
Reference in New Issue
Block a user