Skip to content

Commit

Permalink
Merge pull request #1516 from Vizzuality/MRXN23-459-ability-to-rename…
Browse files Browse the repository at this point in the history
…-scenario

[MRXN23-459]: adds ability to rename scenario
  • Loading branch information
andresgnlez authored Sep 25, 2023
2 parents ad54835 + 8f905f2 commit 5552c75
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 327 deletions.
5 changes: 1 addition & 4 deletions app/hooks/projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,8 @@ export function useSaveProject({
};

return useMutation(saveProject, {
onSuccess: (data: any, variables, context) => {
const { id } = data;
onSuccess: () => {
queryClient.invalidateQueries('projects');
queryClient.invalidateQueries(['project', id]);
console.info('Succces', data, variables, context);
},
onError: (error, variables, context) => {
// An error happened!
Expand Down
238 changes: 238 additions & 0 deletions app/layout/project/sidebar/header/title/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { ComponentProps, HTMLAttributes, useCallback, useRef, useState } from 'react';

import { Form as FormRFF, Field as FieldRFF, FormProps } from 'react-final-form';

import { useRouter } from 'next/router';

import { AnimatePresence, motion } from 'framer-motion';
import { HiOutlinePencil, HiCheck, HiX } from 'react-icons/hi';

import { useCanEditProject } from 'hooks/permissions';

import { composeValidators } from 'components/forms/validations';
import { cn } from 'utils/cn';

export type FormFields = {
name: string;
description: string;
};

const EditableTitle = ({
title,
description,
className,
onEditTitle,
}: {
title: string;
description?: string;
className?: HTMLAttributes<HTMLDivElement>['className'];
onEditTitle: (newName: string) => void;
}): JSX.Element => {
const { query } = useRouter();
const { pid } = query as { pid: string };
const titleInputRef = useRef<HTMLInputElement>(null);

const [editting, setEditting] = useState(false);
const textRefArea = useRef<HTMLTextAreaElement>(null);

const editable = useCanEditProject(pid);

const handleSubmit = useCallback(
(data: Parameters<ComponentProps<typeof FormRFF<FormFields>>['onSubmit']>[0]) => {
titleInputRef.current?.blur();
onEditTitle(data.name);
setEditting(false);
},
[onEditTitle, titleInputRef]
);

const handleEdition = useCallback(() => {
if (!editting && editable && titleInputRef.current) {
setTimeout(() => {
titleInputRef.current.focus();
}, 0);
}
setEditting(true);
}, [titleInputRef, editable, editting]);

const handleCancel = useCallback((form: FormProps<FormFields>['form']) => {
setEditting(false);
form.reset();
}, []);

const invisibleValue = useCallback((value: string) => {
if (value) {
return value.replace(/\s/g, '-');
}
return value;
}, []);

return (
<AnimatePresence exitBeforeEnter>
<motion.div
key="title"
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
>
<FormRFF<FormFields>
onSubmit={handleSubmit}
mutators={{
setTrimName: (args, state, utils) => {
const [name] = args;
utils.changeValue(state, 'name', () => name.trim());
},
}}
initialValues={{
name: title,
description,
}}
>
{(fprops) => (
<form
id="form-title"
onSubmit={fprops.handleSubmit}
autoComplete="off"
className="relative"
>
<div className="flex items-center justify-items-start space-x-1">
<FieldRFF<FormFields['name']>
name="name"
validate={composeValidators([{ presence: true }])}
beforeSubmit={() => {
const { values } = fprops;
fprops.form.mutators.setTrimName(values.name);
}}
>
{({ input }) => (
<div className="relative h-16">
<div
className={cn({
'relative h-full max-w-[260px] overflow-hidden': true,
[className]: Boolean(className),
})}
>
<input
{...input}
ref={titleInputRef}
className="absolute left-0 top-0 h-full w-full cursor-pointer overflow-ellipsis border-none bg-transparent px-1.5 font-heading text-3xl focus:bg-primary-300 focus:text-gray-600 focus:outline-none"
disabled={!editting}
value={input.value}
/>

<h1 className="invisible h-full overflow-ellipsis px-0 py-1 font-heading text-4xl font-normal">
{invisibleValue(input?.value)}
</h1>
</div>
</div>
)}
</FieldRFF>

<div className="relative right-0 flex space-x-0.5">
{editable && !editting && (
<motion.button
key="edit-button"
type="button"
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
className={cn({
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border border-gray-600 transition-colors hover:border-gray-400 focus:outline-none':
true,
'bg-white': editting,
'bg-transparent': !editting,
})}
onClick={handleEdition}
>
<HiOutlinePencil
className={cn({
'h-5 w-5 text-white transition-colors': true,
'text-black': editting,
})}
/>
</motion.button>
)}
{editable && editting && (
<motion.button
key="save-button"
type="submit"
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
className={cn({
'flex h-8 w-8 items-center justify-center rounded-full border border-gray-600 px-2 transition-colors hover:border-gray-400 focus:outline-none':
true,
'bg-white': editting,
})}
>
<HiCheck className="h-4 w-4 text-green-600 transition-colors" />
</motion.button>
)}

{editable && editting && (
<motion.button
key="cancel-button"
type="button"
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
className={cn({
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border border-gray-600 px-2 transition-colors hover:border-gray-400 focus:outline-none':
true,
'bg-white': editting,
})}
onClick={() => handleCancel(fprops.form)}
>
<HiX className="h-3 w-3 text-red-600 transition-colors" />
</motion.button>
)}
</div>
</div>
<FieldRFF<FormFields['description']> name="description">
{({ input }) => (
<div className="relative w-full">
<div className="relative">
<div>
<p className="invisible absolute left-0 top-0 font-heading text-sm font-normal text-white">
{input.value}
</p>
<textarea
{...input}
ref={textRefArea}
className="absolute left-0 top-0 z-50 h-full w-full overflow-ellipsis border-none bg-transparent font-heading text-sm font-normal text-opacity-0 opacity-0 transition-colors focus:bg-primary-300 focus:text-gray-600 focus:opacity-100 focus:outline-none"
disabled={!editting}
value={input.value}
onChange={(v) => {
input.onChange(v);
const textArea = textRefArea.current;
if (textArea) {
textArea.style.height = '';
textArea.style.height = `${textArea.scrollHeight}px`;
}
}}
onFocus={() => {
input.onFocus();
const textArea = textRefArea.current;
if (textArea) {
textArea.style.height = '';
textArea.style.height = `${textArea.scrollHeight}px`;
}
}}
/>
</div>
<p className="line-clamp-3 font-heading text-sm font-normal text-white">
{input.value}
</p>
</div>
</div>
)}
</FieldRFF>
</form>
)}
</FormRFF>
</motion.div>
</AnimatePresence>
);
};

export default EditableTitle;
48 changes: 43 additions & 5 deletions app/layout/project/sidebar/project/header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,53 @@
import { ComponentProps, useCallback } from 'react';

import { useQueryClient } from 'react-query';

import { useRouter } from 'next/router';

import { useProject, useSaveProject } from 'hooks/projects';

import Title from 'layout/project/sidebar/header/title';
import Contributors from 'layout/project/sidebar/project/header/contributors';
import ProjectButton from 'layout/project/sidebar/project/header/project-button';
import ProjectTitle from 'layout/project/sidebar/project/header/title';
import UnderModeration from 'layout/project/sidebar/project/header/under-moderation';

const InventoryProjectHeader = (): JSX.Element => {
const { query } = useRouter();
const { pid } = query as { pid: string };
const queryClient = useQueryClient();

const projectQuery = useProject(pid);
const { name, description } = projectQuery.data;

const saveProjectMutation = useSaveProject({
requestConfig: {
method: 'PATCH',
},
});

const onEditProjectName = useCallback(
(newName: Parameters<ComponentProps<typeof Title>['onEditTitle']>[0]) => {
saveProjectMutation.mutate(
{
id: pid,
data: {
name: newName,
},
},
{
onSuccess: async () => {
await queryClient.invalidateQueries(['project', pid]);
},
}
);
},
[saveProjectMutation, queryClient, pid]
);

return (
<div className="mb-6 flex items-start justify-between">
<div>
<UnderModeration />
<ProjectTitle />
</div>
<UnderModeration />
<Title title={name} description={description} onEditTitle={onEditProjectName} />
<div className="mt-4 flex items-center space-x-5">
<Contributors />
<ProjectButton />
Expand Down
Loading

1 comment on commit 5552c75

@vercel
Copy link

@vercel vercel bot commented on 5552c75 Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

marxan – ./

marxan23.vercel.app
marxan-git-develop-vizzuality1.vercel.app
marxan-vizzuality1.vercel.app

Please sign in to comment.