added events for pages operations

This commit is contained in:
LAKHAN BAHETI 2024-05-29 15:34:04 +05:30
parent 83c8338c64
commit 4932de1ba3
12 changed files with 249 additions and 63 deletions

View File

@ -17,7 +17,9 @@ type Props = {
isOpen: boolean;
projectId: string;
handleClose: () => void;
onResponse: (response: any) => void;
onResponse: (task: string, response: any) => void;
onGenerateResponse?: (task: string, response: any) => void;
onReGenerateResponse?: (task: string, response: any) => void;
onError?: (error: any) => void;
placement?: Placement;
prompt?: string;
@ -33,7 +35,19 @@ type FormData = {
const aiService = new AIService();
export const GptAssistantPopover: React.FC<Props> = (props) => {
const { isOpen, projectId, handleClose, onResponse, onError, placement, prompt, button, className = "" } = props;
const {
isOpen,
projectId,
handleClose,
onResponse,
onGenerateResponse,
onReGenerateResponse,
onError,
placement,
prompt,
button,
className = "",
} = props;
// states
const [response, setResponse] = useState("");
const [invalidResponse, setInvalidResponse] = useState(false);
@ -54,6 +68,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
control,
reset,
setFocus,
getValues,
formState: { isSubmitting },
} = useForm<FormData>({
defaultValues: {
@ -118,6 +133,8 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
}
await callAIService(formData);
if (response !== "" && onReGenerateResponse) onReGenerateResponse(formData.task, response);
else if (response === "" && onGenerateResponse) onGenerateResponse(formData.task, response);
};
useEffect(() => {
@ -162,7 +179,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
<Button
variant="primary"
onClick={() => {
onResponse(response);
onResponse(getValues("task"),response);
onClose();
}}
>
@ -173,8 +190,8 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
const generateResponseButtonText = isSubmitting
? "Generating response..."
: response === ""
? "Generate response"
: "Generate again";
? "Generate response"
: "Generate again";
return (
<Popover as="div" className={`relative w-min text-left`}>

View File

@ -1,15 +1,15 @@
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { FileText } from "lucide-react";
// hooks
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helpers
// components
import { BreadcrumbLink } from "@/components/common";
import { ProjectLogo } from "@/components/project";
import { EUserProjectRoles } from "@/constants/project";
// constants
// components
import { E_PAGES } from "@/constants/event-tracker";
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
export const PagesHeader = observer(() => {
@ -61,7 +61,7 @@ export const PagesHeader = observer(() => {
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Project pages page");
setTrackElement(E_PAGES);
toggleCreatePageModal(true);
}}
>

View File

@ -439,7 +439,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
onResponse={(_,response) => {
handleAiAssistance(response);
}}
placement="top-end"

View File

@ -4,10 +4,12 @@ import { ArchiveRestoreIcon, ExternalLink, Link, Lock, Trash2, UsersRound } from
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { DeletePageModal } from "@/components/pages";
// constants
import { E_PAGES, PAGE_ARCHIVED, PAGE_RESTORED, PAGE_UPDATED } from "@/constants/event-tracker";
// helpers
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { usePage } from "@/hooks/store";
import { usePage, useEventTracker } from "@/hooks/store";
type Props = {
pageId: string;
@ -32,6 +34,7 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
canCurrentUserChangeAccess,
canCurrentUserDeletePage,
} = usePage(pageId);
const { setTrackElement, captureEvent } = useEventTracker();
const pageLink = `${workspaceSlug}/projects/${projectId}/pages/${pageId}`;
const handleCopyText = () =>
@ -48,7 +51,15 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
const MENU_ITEMS: TContextMenuItem[] = [
{
key: "make-public-private",
action: access === 0 ? makePrivate : makePublic,
action: () => {
access === 0 ? makePrivate() : makePublic();
captureEvent(PAGE_UPDATED, {
page_id: pageId,
changed_property: "access",
change_details: access === 0 ? "private" : "public",
element: E_PAGES,
});
},
title: access === 0 ? "Make private" : "Make public",
icon: access === 0 ? Lock : UsersRound,
shouldRender: canCurrentUserChangeAccess && !archived_at,
@ -69,14 +80,23 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
},
{
key: "archive-restore",
action: archived_at ? restore : archive,
action: () => {
archived_at ? restore() : archive();
captureEvent(archived_at ? PAGE_RESTORED : PAGE_ARCHIVED, {
page_id: pageId,
element: E_PAGES,
});
},
title: archived_at ? "Restore" : "Archive",
icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
shouldRender: canCurrentUserArchivePage,
},
{
key: "delete",
action: () => setDeletePageModal(true),
action: () => {
setTrackElement(E_PAGES);
setDeletePageModal(true);
},
title: "Delete",
icon: Trash2,
shouldRender: canCurrentUserDeletePage && !!archived_at,

View File

@ -8,10 +8,12 @@ import { ArchiveIcon } from "@plane/ui";
// components
import { GptAssistantPopover } from "@/components/core";
import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages";
// constants
import { AI_RES_REGENERATED, AI_RES_USED, AI_TRIGGERED, E_PAGES_DETAIL } from "@/constants/event-tracker";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance, useEventTracker } from "@/hooks/store";
// store
import { IPageStore } from "@/store/pages/page.store";
@ -29,12 +31,19 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
const [gptModalOpen, setGptModal] = useState(false);
// store hooks
const { config } = useInstance();
const { captureEvent } = useEventTracker();
// derived values
const { archived_at, isContentEditable, is_locked } = page;
const { archived_at, isContentEditable, is_locked, id } = page;
const handleAiAssistance = async (response: string) => {
const handleAiAssistance = async (task: string, response: string) => {
if (!editorRef) return;
editorRef.current?.setEditorValueAtCursorPosition(response);
captureEvent(AI_RES_USED, {
page_id: id,
element: E_PAGES_DETAIL,
question: task,
answer: response,
});
};
return (
@ -61,6 +70,21 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
// reset(getValues());
}}
onResponse={handleAiAssistance}
onGenerateResponse={(task) =>
captureEvent(AI_TRIGGERED, {
page_id: id,
element: E_PAGES_DETAIL,
question: task,
})
}
onReGenerateResponse={(task, response) =>
captureEvent(AI_RES_REGENERATED, {
page_id: id,
element: E_PAGES_DETAIL,
question: task,
prev_answer: response,
})
}
placement="top-end"
button={
<button

View File

@ -7,11 +7,12 @@ import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@pl
// helpers
import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useAppRouter } from "@/hooks/store";
import { useAppRouter, useEventTracker } from "@/hooks/store";
import { usePageFilters } from "@/hooks/use-page-filters";
// store
import { IPageStore } from "@/store/pages/page.store";
import { E_PAGES_DETAIL, PAGE_ARCHIVED, PAGE_LOCKED, PAGE_RESTORED, PAGE_UNLOCKED } from "@/constants/event-tracker";
type Props = {
editorRef: EditorRefApi | EditorReadOnlyRefApi | null;
@ -33,46 +34,84 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
canCurrentUserDuplicatePage,
canCurrentUserLockPage,
restore,
access,
} = page;
// store hooks
const { workspaceSlug, projectId } = useAppRouter();
const { captureEvent } = useEventTracker();
// page filters
const { isFullWidth, handleFullWidth } = usePageFilters();
const handleArchivePage = async () =>
await archive().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be archived. Please try again later.",
})
);
await archive()
.then(() =>
captureEvent(PAGE_ARCHIVED, {
page_id: id,
access: access === 1 ? "private" : "public",
element: E_PAGES_DETAIL,
state: "SUCCESS",
})
)
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be archived. Please try again later.",
})
);
const handleRestorePage = async () =>
await restore().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be restored. Please try again later.",
})
);
await restore()
.then(() =>
captureEvent(PAGE_RESTORED, {
page_id: id,
access: access === 1 ? "private" : "public",
element: E_PAGES_DETAIL,
state: "SUCCESS",
})
)
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be restored. Please try again later.",
})
);
const handleLockPage = async () =>
await lock().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be locked. Please try again later.",
})
);
await lock()
.then(() =>
captureEvent(PAGE_LOCKED, {
page_id: id,
access: access === 1 ? "private" : "public",
element: E_PAGES_DETAIL,
state: "SUCCESS",
})
)
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be locked. Please try again later.",
})
);
const handleUnlockPage = async () =>
await unlock().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be unlocked. Please try again later.",
})
);
await unlock()
.then(() =>
captureEvent(PAGE_UNLOCKED, {
page_id: id,
access: access === 1 ? "private" : "public",
element: E_PAGES_DETAIL,
state: "SUCCESS",
})
)
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be unlocked. Please try again later.",
})
);
// menu items list
const MENU_ITEMS: {

View File

@ -11,10 +11,13 @@ import {
PageSearchInput,
PageTabNavigation,
} from "@/components/pages";
// constants
import { PAGES_SORT_UPDATED } from "@/constants/event-tracker";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useMember, useProjectPages } from "@/hooks/store";
import { useMember, useProjectPages, useEventTracker } from "@/hooks/store";
import { filter } from "lodash";
type Props = {
pageType: TPageNavigationTabs;
@ -29,6 +32,7 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
const {
workspace: { workspaceMemberIds },
} = useMember();
const { captureEvent } = useEventTracker();
const handleRemoveFilter = useCallback(
(key: keyof TPageFilterProps, value: string | null) => {
@ -59,6 +63,14 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
onChange={(val) => {
if (val.key) updateFilters("sortKey", val.key);
if (val.order) updateFilters("sortBy", val.order);
captureEvent(PAGES_SORT_UPDATED, {
changed_property: val.order ? "sort_by" : "order_by",
change_details: val.order || val.key,
current_sort: {
order_by: filters.sortKey,
sort_by: filters.sortBy,
},
});
}}
/>
<FiltersDropdown

View File

@ -6,10 +6,12 @@ import { Avatar, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// components
import { FavoriteStar } from "@/components/core";
import { PageQuickActions } from "@/components/pages/dropdowns";
// constants
import { E_PAGES, PAGE_FAVORITED, PAGE_UNFAVORITED } from "@/constants/event-tracker";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useMember, usePage } from "@/hooks/store";
import { useMember, usePage, useEventTracker } from "@/hooks/store";
type Props = {
workspaceSlug: string;
@ -24,6 +26,7 @@ export const BlockItemAction: FC<Props> = observer((props) => {
// store hooks
const { access, created_at, is_favorite, owned_by, addToFavorites, removeFromFavorites } = usePage(pageId);
const { getUserDetails } = useMember();
const { captureEvent } = useEventTracker();
// derived values
const ownerDetails = owned_by ? getUserDetails(owned_by) : undefined;
@ -31,21 +34,31 @@ export const BlockItemAction: FC<Props> = observer((props) => {
// handlers
const handleFavorites = () => {
if (is_favorite)
removeFromFavorites().then(() =>
removeFromFavorites().then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page removed from favorites.",
})
);
});
captureEvent(PAGE_UNFAVORITED, {
page_id: pageId,
element: E_PAGES,
state: "SUCCESS",
});
});
else
addToFavorites().then(() =>
addToFavorites().then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page added to favorites.",
})
);
});
captureEvent(PAGE_FAVORITED, {
page_id: pageId,
element: E_PAGES,
state: "SUCCESS",
});
});
};
return (
<>

View File

@ -5,6 +5,10 @@ import { TPageFilterProps, TPageFilters } from "@plane/types";
// components
import { FilterOption } from "@/components/issues";
import { FilterCreatedBy, FilterCreatedDate } from "@/components/pages";
// constants
import { PAGES_FILTER_APPLIED, PAGES_FILTER_REMOVED } from "@/constants/event-tracker";
// hooks
import { useEventTracker } from "@/hooks/store";
type Props = {
filters: TPageFilters;
@ -16,11 +20,13 @@ export const PageFiltersSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, memberIds } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store hooks
const { captureEvent } = useEventTracker();
const handleFilters = (key: keyof TPageFilterProps, value: boolean | string | string[]) => {
const newValues = filters.filters?.[key] ?? [];
if (typeof filters.filters?.[key] === "boolean" && typeof value === "boolean") return;
if (typeof newValues === "boolean" && typeof value === "boolean") return;
const newValues = Array.from((filters.filters?.[key] ?? []) as string[]);
if (Array.isArray(newValues)) {
if (Array.isArray(value))
@ -32,6 +38,16 @@ export const PageFiltersSelection: React.FC<Props> = observer((props) => {
if (newValues?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
captureEvent(
((filters.filters?.[key] ?? []) as string[]).length > newValues.length
? PAGES_FILTER_REMOVED
: PAGES_FILTER_APPLIED,
{
filter_type: key,
filter_property: value,
current_filters: filters?.filters,
}
);
}
handleFiltersUpdate("filters", {
@ -64,12 +80,17 @@ export const PageFiltersSelection: React.FC<Props> = observer((props) => {
<div className="py-2">
<FilterOption
isChecked={!!filters.filters?.favorites}
onClick={() =>
onClick={() => {
handleFiltersUpdate("filters", {
...filters.filters,
favorites: !filters.filters?.favorites,
})
}
});
captureEvent(!filters.filters?.favorites ? PAGES_FILTER_REMOVED : PAGES_FILTER_APPLIED, {
filter_type: "favorites",
filter_property: filters.filters?.favorites,
current_filters: filters,
});
}}
title="Favorites"
/>
</div>

View File

@ -2,8 +2,12 @@ import { FC } from "react";
import Link from "next/link";
// types
import { TPageNavigationTabs } from "@plane/types";
// constants
import { PAGES_TAB_CHANGED, E_PAGES } from "@/constants/event-tracker";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
type TPageTabNavigation = {
workspaceSlug: string;
@ -29,9 +33,15 @@ const pageTabs: { key: TPageNavigationTabs; label: string }[] = [
export const PageTabNavigation: FC<TPageTabNavigation> = (props) => {
const { workspaceSlug, projectId, pageType } = props;
// store hooks
const { captureEvent } = useEventTracker();
const handleTabClick = (e: React.MouseEvent<HTMLAnchorElement>, tabKey: TPageNavigationTabs) => {
if (tabKey === pageType) e.preventDefault();
captureEvent(PAGES_TAB_CHANGED, {
tab: tabKey,
element: E_PAGES,
});
};
return (

View File

@ -178,6 +178,21 @@ export const STATE_DELETED = "State deleted";
export const PAGE_CREATED = "Page created";
export const PAGE_UPDATED = "Page updated";
export const PAGE_DELETED = "Page deleted";
export const PAGE_FAVORITED = "Page favorited";
export const PAGE_UNFAVORITED = "Page unfavorited";
export const PAGE_ARCHIVED = "Page archived";
export const PAGE_LOCKED = "Page locked";
export const PAGE_UNLOCKED = "Page unlocked";
export const PAGE_DUPLICATED = "Page duplicated";
export const PAGE_RESTORED = "Page restored";
export const PAGES_TAB_CHANGED = "Pages tab changed";
export const PAGES_SORT_UPDATED = "Pages sort updated";
export const PAGES_FILTER_APPLIED = "Pages filter applied";
export const PAGES_FILTER_REMOVED = "Pages filter removed";
// AI Assistant Events
export const AI_TRIGGERED = "AI triggered";
export const AI_RES_USED = "AI response used";
export const AI_RES_REGENERATED = "AI response regenerated";
// Member Events
export const MEMBER_INVITED = "Member invited";
export const MEMBER_ACCEPTED = "Member accepted";
@ -222,3 +237,7 @@ export const SNOOZED_NOTIFICATIONS = "Snoozed notifications viewed";
export const ARCHIVED_NOTIFICATIONS = "Archived notifications viewed";
// Groups
export const GROUP_WORKSPACE = "Workspace_metrics";
//Elements
export const E_PAGES = "Pages page"
export const E_PAGES_DETAIL = "Pages detail page"

View File

@ -15,10 +15,12 @@ import { PageHead } from "@/components/core";
import { PageDetailsHeader } from "@/components/headers";
import { IssuePeekOverview } from "@/components/issues";
import { PageEditorBody, PageEditorHeaderRoot } from "@/components/pages";
// constants
import { E_PAGES_DETAIL, PAGE_DUPLICATED } from "@/constants/event-tracker";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { usePage, useProjectPages } from "@/hooks/store";
import { usePage, useProjectPages, useEventTracker } from "@/hooks/store";
// layouts
import { AppLayout } from "@/layouts/app-layout";
// lib
@ -38,7 +40,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
// store hooks
const { createPage, getPageById } = useProjectPages(projectId?.toString() ?? "");
const page = usePage(pageId?.toString() ?? "");
const { description_html, id, name } = page;
const { captureEvent } = useEventTracker();
const { description_html, id, name, access } = page;
// editor markings hook
const { markings, updateMarkings } = useEditorMarkings();
// fetch page details
@ -84,7 +87,15 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
};
await handleCreatePage(formData)
.then((res) => router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res?.id}`))
.then((res) => {
router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res?.id}`);
captureEvent(PAGE_DUPLICATED, {
page_id: pageId,
access: access == 1 ? "private" : "public",
element: E_PAGES_DETAIL,
state: "SUCCESS",
});
})
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,