chore: minor pages UI (#695)

* chore: fix minor ui bugs in pages

* chore: shortcut to add new block

* chore: keyboard accessibility

* chore: block options position
This commit is contained in:
Aaryan Khandelwal 2023-04-04 16:21:46 +05:30 committed by GitHub
parent dad36b404d
commit 2660d646ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 165 additions and 144 deletions

View File

@ -22,6 +22,7 @@ type Props = {
handleClose: () => void;
data?: IPageBlock;
setIsSyncing?: React.Dispatch<React.SetStateAction<boolean>>;
focus?: keyof IPageBlock;
};
const defaultValues = {
@ -38,7 +39,12 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
),
});
export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, setIsSyncing }) => {
export const CreateUpdateBlockInline: React.FC<Props> = ({
handleClose,
data,
setIsSyncing,
focus,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query;
@ -50,6 +56,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
control,
watch,
setValue,
setFocus,
reset,
formState: { isSubmitting },
} = useForm<IPageBlock>({
@ -57,9 +64,10 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
});
const onClose = useCallback(() => {
handleClose();
if (data) handleClose();
reset();
}, [handleClose, reset]);
}, [handleClose, reset, data]);
const createPageBlock = async (formData: Partial<IPageBlock>) => {
if (!workspaceSlug || !projectId || !pageId) return;
@ -126,6 +134,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
};
useEffect(() => {
if (focus) setFocus(focus);
if (!data) return;
reset({
@ -134,7 +144,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
description: data.description,
description_html: data.description_html,
});
}, [reset, data]);
}, [reset, data, focus, setFocus]);
useEffect(() => {
window.addEventListener("keydown", (e: KeyboardEvent) => {
@ -156,9 +166,10 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
name="name"
placeholder="Title"
register={register}
required={true}
className="min-h-10 block w-full resize-none overflow-hidden border-none bg-transparent py-1 text-base ring-0 -ml-2 focus:ring-gray-200"
role="textbox"
autoComplete="off"
maxLength={255}
/>
<div className="page-block-section font relative -mx-3 -mt-3">
<Controller
@ -182,8 +193,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
/>
</div>
<div className="flex justify-end items-center gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" disabled={watch("name") === ""} loading={isSubmitting}>
{data
? isSubmitting
? "Updating..."

View File

@ -42,6 +42,7 @@ type Props = {
block: IPageBlock;
projectDetails: IProject | undefined;
index: number;
handleNewBlock: () => void;
};
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
@ -53,7 +54,12 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
),
});
export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index }) => {
export const SinglePageBlock: React.FC<Props> = ({
block,
projectDetails,
index,
handleNewBlock,
}) => {
const [isSyncing, setIsSyncing] = useState(false);
const [createBlockForm, setCreateBlockForm] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
@ -65,7 +71,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index
const { setToastAlert } = useToast();
const { handleSubmit, watch, reset, setValue, control, register } = useForm<IPageBlock>({
const { handleSubmit, watch, reset, setValue, register } = useForm<IPageBlock>({
defaultValues: {
name: "",
description: {},
@ -273,12 +279,29 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index
reset({ ...block });
}, [reset, block]);
useEffect(() => {
window.addEventListener("keydown", (e: KeyboardEvent) => {
if (e.key === "Enter" && !createBlockForm) handleNewBlock();
});
return () => {
window.removeEventListener("keydown", (e: KeyboardEvent) => {
if (e.key === "Enter" && !createBlockForm) handleNewBlock();
});
};
}, [handleNewBlock, createBlockForm]);
return (
<Draggable draggableId={block.id} index={index}>
<Draggable draggableId={block.id} index={index} isDragDisabled={createBlockForm}>
{(provided, snapshot) => (
<>
{createBlockForm ? (
<div className="mb-4">
<div
className="mb-4 pt-4"
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<CreateUpdateBlockInline
handleClose={() => setCreateBlockForm(false)}
data={block}
@ -287,131 +310,110 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index
</div>
) : (
<div
className={`group ${
snapshot.isDragging
? "border-2 bg-white border-theme shadow-lg rounded-md p-4 pl-0"
: ""
className={`group relative pl-6 ${
snapshot.isDragging ? "border-2 bg-white border-theme shadow-lg rounded-md p-6" : ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className="mb-1 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<button
type="button"
className="flex p-0.5 hover:bg-gray-100 rounded opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
{...provided.dragHandleProps}
>
<EllipsisVerticalIcon className="h-[18px]" />
<EllipsisVerticalIcon className="h-[18px] -ml-3" />
</button>
<h3 className="font-medium" onClick={() => setCreateBlockForm(true)}>
{block.name}
</h3>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
{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">
{isSyncing ? (
<ArrowPathIcon className="h-3 w-3 animate-spin" />
) : (
<CheckIcon className="h-3 w-3" />
)}
{isSyncing ? "Syncing..." : "Synced"}
</div>
)}
{block.issue && (
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${block.issue}`}>
<a className="flex flex-shrink-0 items-center gap-1 rounded bg-gray-100 px-1.5 py-1 text-xs">
<LayerDiagonalIcon height="16" width="16" color="black" />
{projectDetails?.identifier}-{block.issue_detail?.sequence_id}
</a>
</Link>
)}
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100 ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handelAutoGenerateDescription}
disabled={iAmFeelingLucky}
>
{iAmFeelingLucky ? (
"Generating response..."
<button
type="button"
className="absolute top-4 -left-2 p-0.5 hover:bg-gray-100 rounded hidden group-hover:flex"
{...provided.dragHandleProps}
>
<EllipsisVerticalIcon className="h-[18px]" />
<EllipsisVerticalIcon className="h-[18px] -ml-3" />
</button>
<div className="absolute top-4 right-0 items-center gap-2 hidden group-hover:flex bg-white pl-4">
{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">
{isSyncing ? (
<ArrowPathIcon className="h-3 w-3 animate-spin" />
) : (
<>
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
</>
<CheckIcon className="h-3 w-3" />
)}
</button>
<button
type="button"
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
>
<SparklesIcon className="h-4 w-4" />
AI
</button>
<button
type="button"
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
onClick={() => setCreateBlockForm(true)}
>
<PencilIcon className="h-3.5 w-3.5" />
</button>
<CustomMenu label={<BoltIcon className="h-4.5 w-3.5" />} noBorder noChevron>
{block.issue ? (
<>
<CustomMenu.MenuItem onClick={handleBlockSync}>
<>Turn sync {block.sync ? "off" : "on"}</>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>
Copy issue link
</CustomMenu.MenuItem>
</>
) : (
<CustomMenu.MenuItem onClick={pushBlockIntoIssues}>
Push into issues
{isSyncing ? "Syncing..." : "Synced"}
</div>
)}
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100 ${
iAmFeelingLucky ? "cursor-wait bg-gray-100" : ""
}`}
onClick={handelAutoGenerateDescription}
disabled={iAmFeelingLucky}
>
{iAmFeelingLucky ? (
"Generating response..."
) : (
<>
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
</>
)}
</button>
<button
type="button"
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
>
<SparklesIcon className="h-4 w-4" />
AI
</button>
<button
type="button"
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
onClick={() => setCreateBlockForm(true)}
>
<PencilIcon className="h-3.5 w-3.5" />
</button>
<CustomMenu label={<BoltIcon className="h-4.5 w-3.5" />} noBorder noChevron>
{block.issue ? (
<>
<CustomMenu.MenuItem onClick={handleBlockSync}>
<>Turn sync {block.sync ? "off" : "on"}</>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={deletePageBlock}>
Delete block
<CustomMenu.MenuItem onClick={handleCopyText}>
Copy issue link
</CustomMenu.MenuItem>
</>
) : (
<CustomMenu.MenuItem onClick={pushBlockIntoIssues}>
Push into issues
</CustomMenu.MenuItem>
</CustomMenu>
</div>
)}
<CustomMenu.MenuItem onClick={deletePageBlock}>Delete block</CustomMenu.MenuItem>
</CustomMenu>
</div>
<div className="page-block-section font relative -mx-3 -mt-3 ml-6">
<div onClick={() => setCreateBlockForm(true)}>
<Controller
name="description"
control={control}
render={({ field: { value } }) => (
<RemirrorRichTextEditor
value={
!value || (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
placeholder="Description"
customClassName="text-sm"
noBorder
borderOnFocus={false}
editable={false}
/>
)}
/>
</div>
<GptAssistantModal
block={block}
isOpen={gptAssistantModal}
handleClose={() => setGptAssistantModal(false)}
inset="top-2 left-0"
content={block.description_stripped}
htmlContent={block.description_html}
onResponse={handleAiAssistance}
projectId={projectId as string}
/>
<div
className={`flex items-start gap-2 ${
snapshot.isDragging ? "" : "py-4 [&:not(:last-child)]:border-b"
}`}
>
{block.issue && (
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${block.issue}`}>
<a className="flex flex-shrink-0 items-center gap-1 rounded bg-gray-100 px-1.5 py-1 text-xs">
<LayerDiagonalIcon height="16" width="16" color="black" />
{projectDetails?.identifier}-{block.issue_detail?.sequence_id}
</a>
</Link>
)}
<h3
className="font-medium text-sm break-all"
onClick={() => setCreateBlockForm(true)}
>
{block.name}
</h3>
</div>
<GptAssistantModal
block={block}
isOpen={gptAssistantModal}
handleClose={() => setGptAssistantModal(false)}
inset="top-8 left-0"
content={block.description_stripped}
htmlContent={block.description_html}
onResponse={handleAiAssistance}
projectId={projectId as string}
/>
</div>
)}
</>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
@ -54,9 +54,10 @@ import {
} from "constants/fetch-keys";
const SinglePage: NextPage<UserAuth> = (props) => {
const [isAddingBlock, setIsAddingBlock] = useState(false);
const [createBlockForm, setCreateBlockForm] = useState(false);
const scrollToRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query;
@ -238,6 +239,13 @@ const SinglePage: NextPage<UserAuth> = (props) => {
);
};
const handleNewBlock = () => {
setCreateBlockForm(true);
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
};
const options =
labels?.map((label) => ({
value: label.id,
@ -449,14 +457,15 @@ const SinglePage: NextPage<UserAuth> = (props) => {
<DragDropContext onDragEnd={handleOnDragEnd}>
{pageBlocks.length !== 0 && (
<StrictModeDroppable droppableId="blocks-list">
{(provided, snapshot) => (
<div className="" ref={provided.innerRef} {...provided.droppableProps}>
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{pageBlocks.map((block, index) => (
<SinglePageBlock
key={block.id}
block={block}
projectDetails={projectDetails}
index={index}
handleNewBlock={handleNewBlock}
/>
))}
{provided.placeholder}
@ -469,20 +478,19 @@ const SinglePage: NextPage<UserAuth> = (props) => {
<button
type="button"
className="flex items-center gap-1 rounded bg-gray-100 px-2.5 py-1 text-xs hover:bg-gray-200 mt-4"
onClick={() => setCreateBlockForm(true)}
onClick={handleNewBlock}
>
{isAddingBlock ? (
"Adding block..."
) : (
<>
<PlusIcon className="h-3 w-3" />
Add new block
</>
)}
<PlusIcon className="h-3 w-3" />
Add new block
</button>
)}
{createBlockForm && (
<CreateUpdateBlockInline handleClose={() => setCreateBlockForm(false)} />
<div ref={scrollToRef}>
<CreateUpdateBlockInline
handleClose={() => setCreateBlockForm(false)}
focus="name"
/>
</div>
)}
</>
) : (

View File

@ -46,7 +46,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const scollToRef = useRef<HTMLDivElement>(null);
const scrollToRef = useRef<HTMLDivElement>(null);
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
@ -130,7 +130,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
setLabelForm={setLabelForm}
isUpdating={isUpdating}
labelToUpdate={labelToUpdate}
ref={scollToRef}
ref={scrollToRef}
/>
)}
<>
@ -147,7 +147,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
addLabelToGroup={() => addLabelToGroup(label)}
editLabel={(label) => {
editLabel(label);
scollToRef.current?.scrollIntoView({
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}}
@ -163,7 +163,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
addLabelToGroup={addLabelToGroup}
editLabel={(label) => {
editLabel(label);
scollToRef.current?.scrollIntoView({
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}}