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; handleClose: () => void;
data?: IPageBlock; data?: IPageBlock;
setIsSyncing?: React.Dispatch<React.SetStateAction<boolean>>; setIsSyncing?: React.Dispatch<React.SetStateAction<boolean>>;
focus?: keyof IPageBlock;
}; };
const defaultValues = { 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 router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query; const { workspaceSlug, projectId, pageId } = router.query;
@ -50,6 +56,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
control, control,
watch, watch,
setValue, setValue,
setFocus,
reset, reset,
formState: { isSubmitting }, formState: { isSubmitting },
} = useForm<IPageBlock>({ } = useForm<IPageBlock>({
@ -57,9 +64,10 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
}); });
const onClose = useCallback(() => { const onClose = useCallback(() => {
handleClose(); if (data) handleClose();
reset(); reset();
}, [handleClose, reset]); }, [handleClose, reset, data]);
const createPageBlock = async (formData: Partial<IPageBlock>) => { const createPageBlock = async (formData: Partial<IPageBlock>) => {
if (!workspaceSlug || !projectId || !pageId) return; if (!workspaceSlug || !projectId || !pageId) return;
@ -126,6 +134,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
}; };
useEffect(() => { useEffect(() => {
if (focus) setFocus(focus);
if (!data) return; if (!data) return;
reset({ reset({
@ -134,7 +144,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
description: data.description, description: data.description,
description_html: data.description_html, description_html: data.description_html,
}); });
}, [reset, data]); }, [reset, data, focus, setFocus]);
useEffect(() => { useEffect(() => {
window.addEventListener("keydown", (e: KeyboardEvent) => { window.addEventListener("keydown", (e: KeyboardEvent) => {
@ -156,9 +166,10 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
name="name" name="name"
placeholder="Title" placeholder="Title"
register={register} 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" 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" role="textbox"
autoComplete="off"
maxLength={255}
/> />
<div className="page-block-section font relative -mx-3 -mt-3"> <div className="page-block-section font relative -mx-3 -mt-3">
<Controller <Controller
@ -182,8 +193,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({ handleClose, data, se
/> />
</div> </div>
<div className="flex justify-end items-center gap-2"> <div className="flex justify-end items-center gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}> <PrimaryButton type="submit" disabled={watch("name") === ""} loading={isSubmitting}>
{data {data
? isSubmitting ? isSubmitting
? "Updating..." ? "Updating..."

View File

@ -42,6 +42,7 @@ type Props = {
block: IPageBlock; block: IPageBlock;
projectDetails: IProject | undefined; projectDetails: IProject | undefined;
index: number; index: number;
handleNewBlock: () => void;
}; };
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { 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 [isSyncing, setIsSyncing] = useState(false);
const [createBlockForm, setCreateBlockForm] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
@ -65,7 +71,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { handleSubmit, watch, reset, setValue, control, register } = useForm<IPageBlock>({ const { handleSubmit, watch, reset, setValue, register } = useForm<IPageBlock>({
defaultValues: { defaultValues: {
name: "", name: "",
description: {}, description: {},
@ -273,12 +279,29 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index
reset({ ...block }); reset({ ...block });
}, [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 ( return (
<Draggable draggableId={block.id} index={index}> <Draggable draggableId={block.id} index={index} isDragDisabled={createBlockForm}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<> <>
{createBlockForm ? ( {createBlockForm ? (
<div className="mb-4"> <div
className="mb-4 pt-4"
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<CreateUpdateBlockInline <CreateUpdateBlockInline
handleClose={() => setCreateBlockForm(false)} handleClose={() => setCreateBlockForm(false)}
data={block} data={block}
@ -287,131 +310,110 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index
</div> </div>
) : ( ) : (
<div <div
className={`group ${ className={`group relative pl-6 ${
snapshot.isDragging snapshot.isDragging ? "border-2 bg-white border-theme shadow-lg rounded-md p-6" : ""
? "border-2 bg-white border-theme shadow-lg rounded-md p-4 pl-0"
: ""
}`} }`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
> >
<div className="mb-1 flex items-center justify-between gap-2"> <button
<div className="flex items-center gap-2"> type="button"
<button className="absolute top-4 -left-2 p-0.5 hover:bg-gray-100 rounded hidden group-hover:flex"
type="button" {...provided.dragHandleProps}
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" />
<EllipsisVerticalIcon className="h-[18px]" /> </button>
<EllipsisVerticalIcon className="h-[18px] -ml-3" /> <div className="absolute top-4 right-0 items-center gap-2 hidden group-hover:flex bg-white pl-4">
</button> {block.issue && block.sync && (
<h3 className="font-medium" onClick={() => setCreateBlockForm(true)}> <div className="flex flex-shrink-0 cursor-default items-center gap-1 rounded bg-gray-100 py-1 px-1.5 text-xs">
{block.name} {isSyncing ? (
</h3> <ArrowPathIcon className="h-3 w-3 animate-spin" />
</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..."
) : ( ) : (
<> <CheckIcon className="h-3 w-3" />
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
</>
)} )}
</button> {isSyncing ? "Syncing..." : "Synced"}
<button </div>
type="button" )}
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100" <button
onClick={() => setGptAssistantModal((prevData) => !prevData)} type="button"
> className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100 ${
<SparklesIcon className="h-4 w-4" /> iAmFeelingLucky ? "cursor-wait bg-gray-100" : ""
AI }`}
</button> onClick={handelAutoGenerateDescription}
<button disabled={iAmFeelingLucky}
type="button" >
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100" {iAmFeelingLucky ? (
onClick={() => setCreateBlockForm(true)} "Generating response..."
> ) : (
<PencilIcon className="h-3.5 w-3.5" /> <>
</button> <SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
<CustomMenu label={<BoltIcon className="h-4.5 w-3.5" />} noBorder noChevron> </>
{block.issue ? ( )}
<> </button>
<CustomMenu.MenuItem onClick={handleBlockSync}> <button
<>Turn sync {block.sync ? "off" : "on"}</> type="button"
</CustomMenu.MenuItem> className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
<CustomMenu.MenuItem onClick={handleCopyText}> onClick={() => setGptAssistantModal((prevData) => !prevData)}
Copy issue link >
</CustomMenu.MenuItem> <SparklesIcon className="h-4 w-4" />
</> AI
) : ( </button>
<CustomMenu.MenuItem onClick={pushBlockIntoIssues}> <button
Push into issues 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>
)} <CustomMenu.MenuItem onClick={handleCopyText}>
<CustomMenu.MenuItem onClick={deletePageBlock}> Copy issue link
Delete block </CustomMenu.MenuItem>
</>
) : (
<CustomMenu.MenuItem onClick={pushBlockIntoIssues}>
Push into issues
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> )}
</div> <CustomMenu.MenuItem onClick={deletePageBlock}>Delete block</CustomMenu.MenuItem>
</CustomMenu>
</div> </div>
<div className="page-block-section font relative -mx-3 -mt-3 ml-6"> <div
<div onClick={() => setCreateBlockForm(true)}> className={`flex items-start gap-2 ${
<Controller snapshot.isDragging ? "" : "py-4 [&:not(:last-child)]:border-b"
name="description" }`}
control={control} >
render={({ field: { value } }) => ( {block.issue && (
<RemirrorRichTextEditor <Link href={`/${workspaceSlug}/projects/${projectId}/issues/${block.issue}`}>
value={ <a className="flex flex-shrink-0 items-center gap-1 rounded bg-gray-100 px-1.5 py-1 text-xs">
!value || (typeof value === "object" && Object.keys(value).length === 0) <LayerDiagonalIcon height="16" width="16" color="black" />
? watch("description_html") {projectDetails?.identifier}-{block.issue_detail?.sequence_id}
: value </a>
} </Link>
placeholder="Description" )}
customClassName="text-sm" <h3
noBorder className="font-medium text-sm break-all"
borderOnFocus={false} onClick={() => setCreateBlockForm(true)}
editable={false} >
/> {block.name}
)} </h3>
/>
</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> </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> </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"; import { useRouter } from "next/router";
@ -54,9 +54,10 @@ import {
} from "constants/fetch-keys"; } from "constants/fetch-keys";
const SinglePage: NextPage<UserAuth> = (props) => { const SinglePage: NextPage<UserAuth> = (props) => {
const [isAddingBlock, setIsAddingBlock] = useState(false);
const [createBlockForm, setCreateBlockForm] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false);
const scrollToRef = useRef<HTMLDivElement>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query; 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 = const options =
labels?.map((label) => ({ labels?.map((label) => ({
value: label.id, value: label.id,
@ -449,14 +457,15 @@ const SinglePage: NextPage<UserAuth> = (props) => {
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
{pageBlocks.length !== 0 && ( {pageBlocks.length !== 0 && (
<StrictModeDroppable droppableId="blocks-list"> <StrictModeDroppable droppableId="blocks-list">
{(provided, snapshot) => ( {(provided) => (
<div className="" ref={provided.innerRef} {...provided.droppableProps}> <div ref={provided.innerRef} {...provided.droppableProps}>
{pageBlocks.map((block, index) => ( {pageBlocks.map((block, index) => (
<SinglePageBlock <SinglePageBlock
key={block.id} key={block.id}
block={block} block={block}
projectDetails={projectDetails} projectDetails={projectDetails}
index={index} index={index}
handleNewBlock={handleNewBlock}
/> />
))} ))}
{provided.placeholder} {provided.placeholder}
@ -469,20 +478,19 @@ const SinglePage: NextPage<UserAuth> = (props) => {
<button <button
type="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" 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 ? ( <PlusIcon className="h-3 w-3" />
"Adding block..." Add new block
) : (
<>
<PlusIcon className="h-3 w-3" />
Add new block
</>
)}
</button> </button>
)} )}
{createBlockForm && ( {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 router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const scollToRef = useRef<HTMLDivElement>(null); const scrollToRef = useRef<HTMLDivElement>(null);
const { data: projectDetails } = useSWR( const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
@ -130,7 +130,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
setLabelForm={setLabelForm} setLabelForm={setLabelForm}
isUpdating={isUpdating} isUpdating={isUpdating}
labelToUpdate={labelToUpdate} labelToUpdate={labelToUpdate}
ref={scollToRef} ref={scrollToRef}
/> />
)} )}
<> <>
@ -147,7 +147,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
addLabelToGroup={() => addLabelToGroup(label)} addLabelToGroup={() => addLabelToGroup(label)}
editLabel={(label) => { editLabel={(label) => {
editLabel(label); editLabel(label);
scollToRef.current?.scrollIntoView({ scrollToRef.current?.scrollIntoView({
behavior: "smooth", behavior: "smooth",
}); });
}} }}
@ -163,7 +163,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
addLabelToGroup={addLabelToGroup} addLabelToGroup={addLabelToGroup}
editLabel={(label) => { editLabel={(label) => {
editLabel(label); editLabel(label);
scollToRef.current?.scrollIntoView({ scrollToRef.current?.scrollIntoView({
behavior: "smooth", behavior: "smooth",
}); });
}} }}