Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated for comment_submit_shortcut user preference

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("db", "0121_alter_estimate_type"),
]

operations = [
migrations.AddField(
model_name="profile",
name="comment_submit_shortcut",
field=models.CharField(
choices=[("enter", "Enter"), ("mod_enter", "Mod+Enter")],
default="enter",
max_length=20,
),
),
]
7 changes: 7 additions & 0 deletions apps/api/plane/db/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ class NotificationViewMode(models.TextChoices):
FULL = "full", "Full"
COMPACT = "compact", "Compact"

class CommentSubmitShortcut(models.TextChoices):
ENTER = "enter", "Enter"
MOD_ENTER = "mod_enter", "Mod+Enter"

START_OF_THE_WEEK_CHOICES = (
(SUNDAY, "Sunday"),
(MONDAY, "Monday"),
Expand Down Expand Up @@ -243,6 +247,9 @@ class NotificationViewMode(models.TextChoices):
max_length=255, choices=NotificationViewMode.choices, default=NotificationViewMode.FULL
)
is_smooth_cursor_enabled = models.BooleanField(default=False)
comment_submit_shortcut = models.CharField(
max_length=20, choices=CommentSubmitShortcut.choices, default=CommentSubmitShortcut.ENTER
)
# mobile
is_mobile_onboarded = models.BooleanField(default=False)
mobile_onboarding_step = models.JSONField(default=get_mobile_default_onboarding)
Expand Down
5 changes: 5 additions & 0 deletions apps/space/components/editor/lite-text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { cn, isCommentEmpty } from "@plane/utils";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useUserProfile } from "@/hooks/store/use-user-profile";
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
// plane web imports
import { useEditorFlagging } from "@/hooks/use-editor-flagging";
Expand Down Expand Up @@ -57,6 +58,9 @@ export const LiteTextEditor = React.forwardRef(function LiteTextEditor(
}
// derived values
const isEmpty = isCommentEmpty(props.initialValue);
const {
data: { comment_submit_shortcut },
} = useUserProfile();
const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null;
const { liteText: liteTextEditorExtensions } = useEditorFlagging(anchor);
// parse content
Expand All @@ -80,6 +84,7 @@ export const LiteTextEditor = React.forwardRef(function LiteTextEditor(
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
submitShortcut={comment_submit_shortcut}
extendedEditorProps={{}}
{...rest}
// overriding the containerClassName to add relative class passed
Expand Down
1 change: 1 addition & 0 deletions apps/space/store/profile.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class ProfileStore implements IProfileStore {
updated_at: "",
language: "",
start_of_the_week: EStartOfTheWeek.SUNDAY,
comment_submit_shortcut: "enter",
};

// services
Expand Down
19 changes: 17 additions & 2 deletions apps/web/core/components/comments/card/edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import type { EditorRefApi } from "@plane/editor";
import { CheckIcon, CloseIcon } from "@plane/propel/icons";
// plane imports
import type { TCommentsOperations, TIssueComment } from "@plane/types";
import { cn, isCommentEmpty } from "@plane/utils";
import { cn, isCommentEmpty, isCommentSubmissionValid } from "@plane/utils";
// components
import { LiteTextEditor } from "@/components/editor/lite-text";
// hooks
import { useUserProfile } from "@/hooks/store/user";

type Props = {
activityOperations: TCommentsOperations;
Expand All @@ -39,6 +41,10 @@ export const CommentCardEditForm = observer(function CommentCardEditForm(props:
} = props;
// refs
const editorRef = useRef<EditorRefApi>(null);
// store hooks
const {
data: { comment_submit_shortcut },
} = useUserProfile();
// form info
const {
formState: { isSubmitting },
Expand Down Expand Up @@ -77,7 +83,16 @@ export const CommentCardEditForm = observer(function CommentCardEditForm(props:
<form className="flex flex-col gap-2">
<div
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey && !isEmpty) handleSubmit(onEnter)(e);
if (
isCommentSubmissionValid({
e,
shortcut: comment_submit_shortcut,
isEmpty,
isSubmitting,
isEditorReadyToDiscard: !!editorRef.current?.isEditorReadyToDiscard(),
})
)
handleSubmit(onEnter)(e);
}}
>
<LiteTextEditor
Expand Down
20 changes: 12 additions & 8 deletions apps/web/core/components/comments/comment-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { useForm, Controller } from "react-hook-form";
import { EIssueCommentAccessSpecifier } from "@plane/constants";
import type { EditorRefApi } from "@plane/editor";
import type { TIssueComment, TCommentsOperations } from "@plane/types";
import { cn, isCommentEmpty } from "@plane/utils";
import { cn, isCommentEmpty, isCommentSubmissionValid } from "@plane/utils";
// components
import { LiteTextEditor } from "@/components/editor/lite-text";
// hooks
import { useUserProfile } from "@/hooks/store/user";
import { useWorkspace } from "@/hooks/store/use-workspace";
// services
import { FileService } from "@/services/file.service";
Expand Down Expand Up @@ -46,6 +47,9 @@ export const CommentCreate = observer(function CommentCreate(props: TCommentCrea
const editorRef = useRef<EditorRefApi>(null);
// store hooks
const workspaceStore = useWorkspace();
const {
data: { comment_submit_shortcut },
} = useUserProfile();
// derived values
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string;
// form info
Expand Down Expand Up @@ -95,13 +99,13 @@ export const CommentCreate = observer(function CommentCreate(props: TCommentCrea
className={cn("sticky bottom-0 z-[4] bg-surface-1 sm:static")}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
!e.shiftKey &&
!e.ctrlKey &&
!e.metaKey &&
!isEmpty &&
!isSubmitting &&
editorRef.current?.isEditorReadyToDiscard()
isCommentSubmissionValid({
e,
shortcut: comment_submit_shortcut,
isEmpty,
isSubmitting,
isEditorReadyToDiscard: !!editorRef.current?.isEditorReadyToDiscard(),
})
)
handleSubmit(onSubmit)(e);
}}
Expand Down
5 changes: 5 additions & 0 deletions apps/web/core/components/editor/lite-text/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { IssueCommentToolbar } from "@/components/editor/lite-text/toolbar";
// hooks
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
import { useMember } from "@/hooks/store/use-member";
import { useUserProfile } from "@/hooks/store/user";
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
Expand Down Expand Up @@ -95,6 +96,9 @@ export const LiteTextEditor = React.forwardRef(function LiteTextEditor(
});
// store hooks
const { getUserDetails } = useMember();
const {
data: { comment_submit_shortcut },
} = useUserProfile();
// parse content
const { getEditorMetaData } = useParseEditorContent({
projectId,
Expand Down Expand Up @@ -168,6 +172,7 @@ export const LiteTextEditor = React.forwardRef(function LiteTextEditor(
"p-2": !editable,
})}
extendedEditorProps={{}}
submitShortcut={comment_submit_shortcut}
editorClassName={editorClassName}
{...rest}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/

import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import type { CommentSubmitShortcut } from "@plane/types";
import { CustomSelect } from "@plane/ui";
import { getIsMac } from "@plane/utils";
// components
import { SettingsControlItem } from "@/components/settings/control-item";
// hooks
import { useUserProfile } from "@/hooks/store/user";

const isMac = getIsMac();

const COMMENT_SUBMIT_SHORTCUT_OPTIONS: { value: CommentSubmitShortcut; label: string }[] = [
{ value: "enter", label: "Enter" },
{ value: "mod_enter", label: isMac ? "⌘ + Enter" : "Ctrl + Enter" },
];

export const CommentShortcutSelector = observer(function CommentShortcutSelector() {
// store hooks
const {
data: { comment_submit_shortcut },
updateUserProfile,
} = useUserProfile();
// derived values
const selectedLabel = COMMENT_SUBMIT_SHORTCUT_OPTIONS.find((o) => o.value === comment_submit_shortcut)?.label;
// translation
const { t } = useTranslation();

return (
<SettingsControlItem
title={t("comment_submit_shortcut.label")}
description={t("comment_submit_shortcut.description")}
control={
<CustomSelect
value={comment_submit_shortcut}
label={selectedLabel}
onChange={(value: CommentSubmitShortcut) => {
updateUserProfile({ comment_submit_shortcut: value });
}}
buttonClassName="border border-subtle-1"
input
placement="bottom-end"
>
{COMMENT_SUBMIT_SHORTCUT_OPTIONS.map((option) => (
<CustomSelect.Option key={option.value} value={option.value}>
{option.label}
</CustomSelect.Option>
))}
</CustomSelect>
}
/>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import { observer } from "mobx-react";
// components
import { ThemeSwitcher } from "@/plane-web/components/preferences/theme-switcher";
// local imports
import { CommentShortcutSelector } from "./comment-shortcut-selector";

export const ProfileSettingsDefaultPreferencesList = observer(function ProfileSettingsDefaultPreferencesList() {
return (
Expand All @@ -18,6 +20,7 @@ export const ProfileSettingsDefaultPreferencesList = observer(function ProfileSe
description: "select_or_customize_your_interface_color_scheme",
}}
/>
<CommentShortcutSelector />
</div>
);
});
1 change: 1 addition & 0 deletions apps/web/core/store/user/profile.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class ProfileStore implements IUserProfileStore {
updated_at: "",
language: "",
start_of_the_week: EStartOfTheWeek.SUNDAY,
comment_submit_shortcut: "enter",
};

// services
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ import { EnterKeyExtension } from "@/extensions";
import type { EditorRefApi, ILiteTextEditorProps } from "@/types";

function LiteTextEditor(props: ILiteTextEditorProps) {
const { onEnterKeyPress, disabledExtensions, extensions: externalExtensions = [] } = props;
const { onEnterKeyPress, submitShortcut, disabledExtensions, extensions: externalExtensions = [] } = props;

const extensions = useMemo(() => {
const resolvedExtensions = [...externalExtensions];

if (!disabledExtensions?.includes("enter-key")) {
resolvedExtensions.push(EnterKeyExtension(onEnterKeyPress));
resolvedExtensions.push(EnterKeyExtension({ onEnterKeyPress, submitShortcut }));
}

return resolvedExtensions;
}, [externalExtensions, disabledExtensions, onEnterKeyPress]);
}, [externalExtensions, disabledExtensions, onEnterKeyPress, submitShortcut]);

return <EditorWrapper {...props} extensions={extensions} />;
}
Expand Down
72 changes: 49 additions & 23 deletions packages/editor/src/core/extensions/enter-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,61 @@
* See the LICENSE file for details.
*/

import type { Editor } from "@tiptap/core";
import { Extension } from "@tiptap/core";
// plane imports
import type { CommentSubmitShortcut } from "@plane/types";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";

export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
Extension.create({
name: CORE_EXTENSIONS.ENTER_KEY,
type EnterKeyExtensionArgs = {
onEnterKeyPress?: () => void;
submitShortcut?: CommentSubmitShortcut;
};

addKeyboardShortcuts(this) {
return {
Enter: () => {
const { activeDropbarExtensions } = this.editor.storage.utility;
export const EnterKeyExtension = ({ onEnterKeyPress, submitShortcut = "enter" }: EnterKeyExtensionArgs) => {
const newLine = ({ editor }: { editor: Editor }) =>
editor.commands.first(({ commands }) => [
() => commands.newlineInCode(),
() => commands.splitListItem(CORE_EXTENSIONS.LIST_ITEM),
() => commands.splitListItem(CORE_EXTENSIONS.TASK_ITEM),
() => commands.createParagraphNear(),
() => commands.liftEmptyBlock(),
() => commands.splitBlock(),
]);

if (activeDropbarExtensions.length === 0) {
onEnterKeyPress?.();
return true;
}
const submit = (editor: Editor) => {
const activeDropbarExtensions = editor.storage.utility?.activeDropbarExtensions ?? [];
if (activeDropbarExtensions.length === 0) {
onEnterKeyPress?.();
return true;
}
return false;
};

return false;
},
"Shift-Enter": ({ editor }) =>
editor.commands.first(({ commands }) => [
() => commands.newlineInCode(),
() => commands.splitListItem(CORE_EXTENSIONS.LIST_ITEM),
() => commands.splitListItem(CORE_EXTENSIONS.TASK_ITEM),
() => commands.createParagraphNear(),
() => commands.liftEmptyBlock(),
() => commands.splitBlock(),
]),
};
return Extension.create({
name: CORE_EXTENSIONS.ENTER_KEY,

addKeyboardShortcuts(this) {
if (submitShortcut === "enter") {
return {
// Shift+Enter always inserts a new line (its default behavior)
"Shift-Enter": newLine,
// Enter submits
Enter: () => submit(this.editor),
// Mod+Enter always inserts a line-break (its default behavior)
"Mod-Enter": () => false,
};
} else {
return {
// Shift+Enter always inserts a line-break (its default behavior)
"Shift-Enter": () => false,
// Enter always inserts a new line (its default behavior)
Enter: () => false,
// Mod+Enter submits
"Mod-Enter": () => submit(this.editor),
};
}
},
});
};
5 changes: 4 additions & 1 deletion packages/editor/src/core/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Selection } from "@tiptap/pm/state";
import type { EditorProps, EditorView } from "@tiptap/pm/view";
import type { NodeViewProps as TNodeViewProps } from "@tiptap/react";
// plane imports
import type { CommentSubmitShortcut } from "@plane/types";
import type { TCustomComponentsMetaData } from "@plane/utils";
// extension types
import type { TTextAlign } from "@/extensions";
Expand Down Expand Up @@ -184,7 +185,9 @@ export type IEditorProps = {
workItemIdentifier?: string | null;
};

export type ILiteTextEditorProps = IEditorProps;
export type ILiteTextEditorProps = IEditorProps & {
submitShortcut?: CommentSubmitShortcut;
};

export type IRichTextEditorProps = IEditorProps & {
dragDropEnabled?: boolean;
Expand Down
Loading
Loading