Skip to content

Commit

Permalink
Feat: create issue/sub-issue from comments
Browse files Browse the repository at this point in the history
  • Loading branch information
harshithmullapudi committed Oct 1, 2024
1 parent 11d7dea commit cacc549
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './tegon-issue-extension';
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NodeViewWrapper } from '@tiptap/react';
import { getWorkflowColor } from 'common/status-color';
import { getWorkflowIcon } from 'common/workflow-icons';
import { useTeamWorkflows } from 'hooks/workflows';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import React from 'react';
import { useContextStore } from 'store/global-context-provider';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const TegonIssueComponent = (props: any) => {
const { workspaceSlug } = useParams<{ workspaceSlug: string }>();
const { issuesStore, teamsStore } = useContextStore();

const url = props.node.attrs.url;
const identifier = url.split('/')[url.split('/').length - 1];
const teamIdentifier = identifier.split('-')[0];

const team = teamsStore.getTeamWithIdentifier(identifier.split('-')[0]);
const workflows = useTeamWorkflows(teamIdentifier);

const issue = team
? issuesStore.getIssueByNumber(identifier, team.id)
: undefined;

if (!issue) {
return (
<NodeViewWrapper className="react-component-with-content">
<div className="content">{url}</div>
</NodeViewWrapper>
);
}

const workflow = workflows.find((workflow) => workflow.id === issue.stateId);

const CategoryIcon = getWorkflowIcon(workflow);
return (
<NodeViewWrapper className="react-component-with-content">
<div className="content">
<Link
className="flex gap-1 bg-grayAlpha-100 p-1 px-2 w-fit rounded items-center"
href={`/${workspaceSlug}/issue/${team.identifier}-${issue.number}`}
>
<CategoryIcon size={20} color={getWorkflowColor(workflow).color} />
{issue.title}
</Link>
</div>
</NodeViewWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { mergeAttributes, Node } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';

import { TegonIssueComponent } from './tegon-issue-component';

export const tegonIssueExtension = Node.create({
name: 'tegonIssueExtension',
group: 'block',
atom: true,

addAttributes() {
return {
url: {
default: undefined,
},
};
},

parseHTML() {
return [
{
tag: 'tegon-issue-extension',
},
];
},

renderHTML({ HTMLAttributes }) {
return ['tegon-issue-extension', mergeAttributes(HTMLAttributes)];
},

addNodeView() {
return ReactNodeViewRenderer(TegonIssueComponent);
},
});
53 changes: 53 additions & 0 deletions apps/webapp/src/hooks/use-editor-paste-handler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { EditorT } from '@tegonhq/ui/components/editor/index';
import { useContextStore } from 'store/global-context-provider';

// Updates the height of a <textarea> when the value changes.
export const useEditorPasteHandler = () => {
const { issuesStore, teamsStore } = useContextStore();

const handlePaste = (editor: EditorT, event: ClipboardEvent) => {
const pastedText = event.clipboardData.getData('text/plain');
const regex = /https?:\/\/app\.tegon\.ai\/\w+\/issue\/([A-Z]+)-(\d+)/;
const isTegonIssue = regex.test(pastedText);
const parts = regex.exec(pastedText);
if (isTegonIssue && parts) {
const teamIdentifier = parts[1]; // 'ENG' in this case
const issueId = parts[2]; // '11' in this case
const team = teamsStore.getTeamWithIdentifier(teamIdentifier);
const issue = team
? issuesStore.getIssueByNumber(`${teamIdentifier}-${issueId}`, team.id)
: undefined;

if (issue) {
editor
.chain()
.insertContentAt(editor.view.state.selection.from, [
{
type: 'tegonIssueExtension',
attrs: {
url: pastedText,
},
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: '\n',
},
],
},
])
.exitCode()
.focus()
.run();

return true;
}
}

return false;
};

return { handlePaste };
};
5 changes: 4 additions & 1 deletion apps/webapp/src/modules/issues/new-issue/new-issue-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function NewIssueForm({
index,
isLoading,
onClose,
parentId,
subIssueOperations,
}: NewIssueFormProps) {
const issue = useWatch({
Expand Down Expand Up @@ -96,7 +97,9 @@ export function NewIssueForm({

return (
<div className="flex flex-col overflow-hidden">
<NewIssueTitle isSubIssue={isSubIssue} form={form} index={index} />
<div className="flex flex-wrap gap-2">
<NewIssueTitle isSubIssue={isSubIssue} form={form} index={index} />
</div>

<div className="flex flex-col gap-2 p-4 pt-0 overflow-hidden">
<FormField
Expand Down
12 changes: 9 additions & 3 deletions apps/webapp/src/modules/issues/new-issue/new-issue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,27 @@ import { IssueCollapseView } from './issue-collapse-view';
import { NewIssueForm } from './new-issue-form';
import { NewIssueSchema } from './new-issues-type';

interface NewIssueProps {
export interface IssueDefaultValues {
parentId?: string;
description?: string;
}

interface NewIssueProps {
defaultValues?: IssueDefaultValues;
open: boolean;
setOpen: (value: boolean) => void;
}

export function NewIssue({ open, setOpen, parentId }: NewIssueProps) {
export function NewIssue({ open, setOpen, defaultValues = {} }: NewIssueProps) {
useScope(SCOPES.NewIssue);
const { toast } = useToast();
const { parentId, description } = defaultValues;

// The form has a array of issues where first issue is the parent and the later sub issues
const form = useForm<z.infer<typeof NewIssueSchema>>({
resolver: zodResolver(NewIssueSchema),
defaultValues: {
issues: [{ parentId }],
issues: [{ parentId, description }],
},
});

Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/src/modules/issues/single-issue/issue-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ export const IssueView = observer(() => {
<div className="flex flex-col h-[100vh]">
<Header />
<main className="grid grid-cols-5 h-[calc(100vh_-_53px)] bg-background-2 rounded-tl-3xl">
<div className="col-span-5 xl:col-span-4 flex flex-col h-[calc(100vh_-_55px)]">
<div className="col-span-4 flex flex-col h-[calc(100vh_-_55px)]">
<LeftSide />
</div>
<div className="border-l border-border hidden flex-col xl:flex">
<div className="border-l border-border flex-col flex">
<RightSide />
</div>
</main>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
EditorExtensions,
suggestionItems,
} from '@tegonhq/ui/components/editor/index';
import { EditLine, MoreLine } from '@tegonhq/ui/icons';
import { EditLine, MoreLine, NewIssueLine, SubIssue } from '@tegonhq/ui/icons';
import { cn } from '@tegonhq/ui/lib/utils';
import * as React from 'react';
import ReactTimeAgo from 'react-time-ago';
Expand All @@ -24,6 +24,8 @@ import { UserContext } from 'store/user-context';
import { EditComment } from './edit-comment';
import { ReplyComment } from './reply-comment';
import { getUserDetails } from '../issue-activity/user-activity-utils';
import { NewIssue } from 'modules/issues/new-issue';
import { useIssueData } from 'hooks/issues';

export interface GenericCommentActivityProps {
comment: IssueCommentType;
Expand All @@ -44,11 +46,16 @@ export function GenericCommentActivity(props: GenericCommentActivityProps) {
getUserData,
} = props;
const currentUser = React.useContext(UserContext);
const issue = useIssueData();

const sourceMetadata = comment.sourceMetadata
? JSON.parse(comment.sourceMetadata)
: undefined;

const [edit, setEdit] = React.useState(false);
const [defaultIssueCreationValues, setDefaultIssueCreationValues] =
React.useState(undefined);
const [newIssueDialog, setNewIssueDialog] = React.useState(false);

return (
<div className="flex items-start">
Expand Down Expand Up @@ -76,24 +83,53 @@ export function GenericCommentActivity(props: GenericCommentActivityProps) {
</div>

<div className="flex gap-2 items-center">
{!sourceMetadata && user.id === currentUser.id && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="px-2 py-0 h-5">
<MoreLine size={16} className="text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="px-2 py-0 h-5">
<MoreLine size={16} className="text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
{!sourceMetadata && user.id === currentUser.id && (
<DropdownMenuItem onClick={() => setEdit(true)}>
<div className="flex items-center gap-1">
<EditLine size={16} className="mr-1" /> Edit
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
)}

<DropdownMenuItem
onClick={() => {
setDefaultIssueCreationValues({
description: comment.body,
});
setNewIssueDialog(true);
}}
>
<div className="flex items-center gap-1">
<NewIssueLine size={16} className="mr-1" /> New issue from
comment
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setDefaultIssueCreationValues({
parentId: issue.id,
description: comment.body,
});
setNewIssueDialog(true);
}}
>
<div className="flex items-center gap-1">
<SubIssue size={16} className="mr-1" /> Sub issue from
comment
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

<div>
<ReactTimeAgo
date={new Date(comment.updatedAt)}
Expand Down Expand Up @@ -152,6 +188,14 @@ export function GenericCommentActivity(props: GenericCommentActivityProps) {

{allowReply && <ReplyComment issueCommentId={comment.id} />}
</div>

{newIssueDialog && (
<NewIssue
open={newIssueDialog}
setOpen={setNewIssueDialog}
defaultValues={defaultIssueCreationValues}
/>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const FilterSmall = observer(() => {
};

return (
<div className="my-1 flex gap-2">
<div className="my-2 flex gap-2 bg-background-3 rounded p-2">
<IssueStatusDropdown
value={issue.stateId}
onChange={statusChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ import { useTeamWorkflows } from 'hooks/workflows';
import { useUpdateIssueMutation } from 'services/issues';

import { Activity } from './activity';
import { FilterSmall } from './filters-small';
import { IssueTitle } from './issue-title';
import { LinkedIssuesView } from './linked-issues-view';
import { ParentIssueView } from './parent-issue-view';
import { SimilarIssuesView } from './similar-issues-view';
import { SubIssueView } from './sub-issue-view';
import { tegonIssueExtension } from 'common/editor/tegon-issue-extension';
import { useEditorPasteHandler } from 'hooks/use-editor-paste-handler';

export const LeftSide = observer(() => {
const issue = useIssueData();
Expand Down Expand Up @@ -57,13 +58,15 @@ export const LeftSide = observer(() => {
});
}, 1000);

const { handlePaste } = useEditorPasteHandler();

return (
<ScrollArea className="grow flex h-full justify-center w-full">
<div className="flex h-full justify-center w-full">
<div className="grow flex flex-col gap-2 h-full max-w-[97ch]">
<div className="flex xl:hidden px-6 py-2 border-b">
{/* <div className="flex xl:hidden px-6 py-2 border-b">
<FilterSmall />
</div>
</div> */}
<div className="py-6 flex flex-col">
{isTriageView && <SimilarIssuesView issueId={issue.id} />}

Expand All @@ -76,6 +79,8 @@ export const LeftSide = observer(() => {
<Editor
value={issue.description}
onChange={onDescriptionChange}
handlePaste={handlePaste}
extensions={[tegonIssueExtension]}
className="min-h-[50px] mb-8 px-6 mt-3 text-md"
>
<EditorExtensions suggestionItems={suggestionItems}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function SubIssueView({ childIssues, issueId }: SubIssueViewProps) {
<NewIssue
open={newIssueDialog}
setOpen={setNewIssueDialog}
parentId={issueId}
defaultValues={{ parentId: issueId }}
/>
</>
);
Expand Down
7 changes: 7 additions & 0 deletions apps/webapp/src/store/teams/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ export const TeamsStore: IAnyStateTreeNode = types

return team;
},
getTeamWithIdentifier(teamIdentifier: string) {
const team = self.teams.find((team: TeamType) => {
return team.identifier === teamIdentifier;
});

return team;
},
get getTeams() {
return self.teams;
},
Expand Down
Loading

0 comments on commit cacc549

Please sign in to comment.