mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-986] fix: editor slash command positioning (#4186)
* fix: table selected cell border * chore: add syncing message when revalidating page data * fix: slash command positioning * fix: mentions list dropdown positioning
This commit is contained in:
parent
7a21855ab6
commit
8454e4f1e0
@ -49,15 +49,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom list item styles */
|
|
||||||
|
|
||||||
/* Custom gap cursor styles */
|
/* Custom gap cursor styles */
|
||||||
.ProseMirror-gapcursor::after {
|
.ProseMirror-gapcursor::after {
|
||||||
border-top: 1px solid rgb(var(--color-text-100)) !important;
|
border-top: 1px solid rgb(var(--color-text-100)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
|
/* to-do list */
|
||||||
|
|
||||||
ul[data-type="taskList"] li {
|
ul[data-type="taskList"] li {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@ -143,6 +140,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
|||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
}
|
}
|
||||||
|
/* end to-do list */
|
||||||
|
|
||||||
/* Overwrite tippy-box original max-width */
|
/* Overwrite tippy-box original max-width */
|
||||||
|
|
||||||
@ -238,7 +236,7 @@ div[data-type="horizontalRule"] {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
border-bottom: 1px solid rgb(var(--color-text-100));
|
border-bottom: 1px solid rgb(var(--color-border-200));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +275,7 @@ span:focus .fake-cursor {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* number, bulleted and to-do lists */
|
/* numbered, bulleted and to-do lists spacing */
|
||||||
.prose ol:where(.prose > :first-child):not(:where([class~="not-prose"], [class~="not-prose"] *)),
|
.prose ol:where(.prose > :first-child):not(:where([class~="not-prose"], [class~="not-prose"] *)),
|
||||||
.prose
|
.prose
|
||||||
ul:not([data-type="taskList"]):where(.prose > :first-child):not(:where([class~="not-prose"], [class~="not-prose"] *)),
|
ul:not([data-type="taskList"]):where(.prose > :first-child):not(:where([class~="not-prose"], [class~="not-prose"] *)),
|
||||||
@ -306,7 +304,7 @@ ul:not([data-type="taskList"]) ol {
|
|||||||
ul[data-type="taskList"] ul[data-type="taskList"] {
|
ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
}
|
}
|
||||||
/* end number, bulleted and to-do lists */
|
/* end numbered, bulleted and to-do lists spacing */
|
||||||
|
|
||||||
/* tailwind typography */
|
/* tailwind typography */
|
||||||
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
|
@ -31,17 +31,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrapper table td > *,
|
|
||||||
.table-wrapper table th > * {
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0.25rem 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrapper table td.has-focus,
|
|
||||||
.table-wrapper table th.has-focus {
|
|
||||||
box-shadow: rgba(var(--color-primary-300), 0.1) 0px 0px 0px 2px inset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrapper table th {
|
.table-wrapper table th {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -49,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-wrapper table .selectedCell {
|
.table-wrapper table .selectedCell {
|
||||||
border-color: rgba(var(--color-primary-100));
|
outline: 0.5px solid rgba(var(--color-primary-100));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* table dropdown */
|
/* table dropdown */
|
||||||
|
@ -69,7 +69,7 @@ export const CoreEditorExtensions = (
|
|||||||
CustomQuoteExtension,
|
CustomQuoteExtension,
|
||||||
CustomHorizontalRule.configure({
|
CustomHorizontalRule.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "my-4",
|
class: "my-4 border-custom-border-400",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
CustomKeymap,
|
CustomKeymap,
|
||||||
|
@ -135,7 +135,7 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={commandListContainer}
|
ref={commandListContainer}
|
||||||
className="mentions absolute max-h-48 min-w-[12rem] rounded-md bg-custom-background-100 border-[0.5px] border-custom-border-300 px-2 py-2.5 text-xs shadow-custom-shadow-rg overflow-y-scroll"
|
className="mentions max-h-48 min-w-[12rem] rounded-md bg-custom-background-100 border-[0.5px] border-custom-border-300 px-2 py-2.5 text-xs shadow-custom-shadow-rg overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center text-custom-text-400">Loading...</div>
|
<div className="text-center text-custom-text-400">Loading...</div>
|
||||||
|
@ -247,14 +247,15 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
|
const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
|
||||||
|
// states
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
// refs
|
||||||
|
const commandListContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const selectItem = useCallback(
|
const selectItem = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const item = items[index];
|
const item = items[index];
|
||||||
if (item) {
|
if (item) command(item);
|
||||||
command(item);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[command, items]
|
[command, items]
|
||||||
);
|
);
|
||||||
@ -289,8 +290,6 @@ const CommandList = ({ items, command }: { items: CommandItemProps[]; command: a
|
|||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const commandListContainer = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const container = commandListContainer?.current;
|
const container = commandListContainer?.current;
|
||||||
|
|
||||||
@ -299,29 +298,31 @@ const CommandList = ({ items, command }: { items: CommandItemProps[]; command: a
|
|||||||
if (item && container) updateScrollView(container, item);
|
if (item && container) updateScrollView(container, item);
|
||||||
}, [selectedIndex]);
|
}, [selectedIndex]);
|
||||||
|
|
||||||
return items.length > 0 ? (
|
if (items.length <= 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
id="slash-command"
|
id="slash-command"
|
||||||
ref={commandListContainer}
|
ref={commandListContainer}
|
||||||
className="fixed z-50 h-auto max-h-[330px] w-52 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-100 px-1 py-2 shadow-md transition-all"
|
className="z-10 max-h-80 min-w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg"
|
||||||
>
|
>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<button
|
<button
|
||||||
key={item.key}
|
key={item.key}
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-custom-text-100 hover:bg-custom-background-80`,
|
"flex items-center gap-2 w-full rounded px-1 py-1.5 text-sm text-left truncate text-custom-text-200 hover:bg-custom-background-80",
|
||||||
{
|
{
|
||||||
"bg-custom-background-80": index === selectedIndex,
|
"bg-custom-background-80": index === selectedIndex,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={() => selectItem(index)}
|
onClick={() => selectItem(index)}
|
||||||
>
|
>
|
||||||
<div className="grid flex-shrink-0 place-items-center">{item.icon}</div>
|
<span className="grid place-items-center flex-shrink-0">{item.icon}</span>
|
||||||
<p>{item.title}</p>
|
<p className="flex-grow truncate">{item.title}</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CommandListInstance {
|
interface CommandListInstance {
|
||||||
@ -338,10 +339,12 @@ const renderItems = () => {
|
|||||||
editor: props.editor,
|
editor: props.editor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tippyContainer = document.querySelector(".active-editor") ?? document.querySelector("#editor-container");
|
||||||
|
|
||||||
// @ts-expect-error Tippy overloads are messed up
|
// @ts-expect-error Tippy overloads are messed up
|
||||||
popup = tippy("body", {
|
popup = tippy("body", {
|
||||||
getReferenceClientRect: props.clientRect,
|
getReferenceClientRect: props.clientRect,
|
||||||
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
|
appendTo: tippyContainer,
|
||||||
content: component.element,
|
content: component.element,
|
||||||
showOnCreate: true,
|
showOnCreate: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
|
@ -19,13 +19,14 @@ import { IPageStore } from "@/store/pages/page.store";
|
|||||||
type Props = {
|
type Props = {
|
||||||
editorRef: React.RefObject<EditorRefApi>;
|
editorRef: React.RefObject<EditorRefApi>;
|
||||||
handleDuplicatePage: () => void;
|
handleDuplicatePage: () => void;
|
||||||
|
isSyncing: boolean;
|
||||||
pageStore: IPageStore;
|
pageStore: IPageStore;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
|
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
||||||
const { editorRef, handleDuplicatePage, pageStore, projectId, readOnlyEditorRef } = props;
|
const { editorRef, handleDuplicatePage, isSyncing, pageStore, projectId, readOnlyEditorRef } = props;
|
||||||
// states
|
// states
|
||||||
const [gptModalOpen, setGptModal] = useState(false);
|
const [gptModalOpen, setGptModal] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
@ -52,6 +53,12 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
|||||||
<span className="text-sm text-custom-text-300">{isSubmitting === "submitting" ? "Saving..." : "Saved"}</span>
|
<span className="text-sm text-custom-text-300">{isSubmitting === "submitting" ? "Saving..." : "Saved"}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isSyncing && (
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<RefreshCw className="h-4 w-4 stroke-custom-text-300" />
|
||||||
|
<span className="text-sm text-custom-text-300">Syncing...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{is_locked && (
|
{is_locked && (
|
||||||
<div className="flex h-7 items-center gap-2 rounded-full bg-custom-background-80 px-3 py-0.5 text-xs font-medium text-custom-text-300">
|
<div className="flex h-7 items-center gap-2 rounded-full bg-custom-background-80 px-3 py-0.5 text-xs font-medium text-custom-text-300">
|
||||||
<Lock className="h-3 w-3" />
|
<Lock className="h-3 w-3" />
|
||||||
|
@ -11,6 +11,7 @@ type Props = {
|
|||||||
editorRef: React.RefObject<EditorRefApi>;
|
editorRef: React.RefObject<EditorRefApi>;
|
||||||
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
|
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
|
||||||
handleDuplicatePage: () => void;
|
handleDuplicatePage: () => void;
|
||||||
|
isSyncing: boolean;
|
||||||
markings: IMarking[];
|
markings: IMarking[];
|
||||||
pageStore: IPageStore;
|
pageStore: IPageStore;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -28,6 +29,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
|||||||
markings,
|
markings,
|
||||||
readOnlyEditorReady,
|
readOnlyEditorReady,
|
||||||
handleDuplicatePage,
|
handleDuplicatePage,
|
||||||
|
isSyncing,
|
||||||
pageStore,
|
pageStore,
|
||||||
projectId,
|
projectId,
|
||||||
sidePeekVisible,
|
sidePeekVisible,
|
||||||
@ -61,6 +63,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
|||||||
<PageExtraOptions
|
<PageExtraOptions
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
handleDuplicatePage={handleDuplicatePage}
|
handleDuplicatePage={handleDuplicatePage}
|
||||||
|
isSyncing={isSyncing}
|
||||||
pageStore={pageStore}
|
pageStore={pageStore}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
readOnlyEditorRef={readOnlyEditorRef}
|
readOnlyEditorRef={readOnlyEditorRef}
|
||||||
|
@ -46,7 +46,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// fetching page details
|
// fetching page details
|
||||||
const { data: swrPageDetails } = useSWR(
|
const { data: swrPageDetails, isValidating } = useSWR(
|
||||||
pageId ? `PAGE_DETAILS_${pageId}` : null,
|
pageId ? `PAGE_DETAILS_${pageId}` : null,
|
||||||
pageId ? () => getPageById(pageId.toString()) : null,
|
pageId ? () => getPageById(pageId.toString()) : null,
|
||||||
{
|
{
|
||||||
@ -120,6 +120,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
editorReady={editorReady}
|
editorReady={editorReady}
|
||||||
readOnlyEditorReady={readOnlyEditorReady}
|
readOnlyEditorReady={readOnlyEditorReady}
|
||||||
handleDuplicatePage={handleDuplicatePage}
|
handleDuplicatePage={handleDuplicatePage}
|
||||||
|
isSyncing={isValidating}
|
||||||
markings={markings}
|
markings={markings}
|
||||||
pageStore={pageStore}
|
pageStore={pageStore}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
|
Loading…
Reference in New Issue
Block a user