From 5017b6bf87692ad14c5fe0b75f4948a3869df8a6 Mon Sep 17 00:00:00 2001 From: Leonardo Negreiros de Oliveira Date: Mon, 2 Oct 2023 09:59:11 -0300 Subject: [PATCH] Create task form (#680) * Remove NextJs default styles We will be using JoyUI theme for now so its safe to delete this * Add ReactQuery to project This lib is being installed to facilitate the communication with the API and the state management for it. * Create hooks to fetch projects and tasktypes * Create Form page * Add reset form action * Switch Select for Autocomplete * Refactor useForm types * Add loading to select * Improve Buttons positioning * Fix Autocomplete clear * Select Option on autocomplete with Tab * Adjust Task to TaskType * Reorder TaskForm fields --- frontend/package-lock.json | 64 ++++++++++ frontend/package.json | 1 + frontend/src/app/globals.css | 88 -------------- frontend/src/app/layout.tsx | 26 ++--- frontend/src/app/providers.tsx | 18 +++ frontend/src/app/tasks/hooks/useForm.ts | 17 +++ frontend/src/app/tasks/hooks/useProjects.ts | 39 +++++++ frontend/src/app/tasks/hooks/useTaskTypes.ts | 36 ++++++ frontend/src/app/tasks/page.tsx | 109 ++++++++++++++++++ frontend/src/ui/Input/Input.tsx | 89 ++++++++++++++ frontend/src/ui/Select/Select.tsx | 52 +++++++++ frontend/src/ui/Sidebar/Sidebar.tsx | 8 +- .../src/ui/SkipNavigation/SkipNavigation.tsx | 2 +- frontend/src/ui/TextArea/TextArea.tsx | 83 +++++++++++++ frontend/src/ui/theme.ts | 18 +++ 15 files changed, 542 insertions(+), 108 deletions(-) create mode 100644 frontend/src/app/providers.tsx create mode 100644 frontend/src/app/tasks/hooks/useForm.ts create mode 100644 frontend/src/app/tasks/hooks/useProjects.ts create mode 100644 frontend/src/app/tasks/hooks/useTaskTypes.ts create mode 100644 frontend/src/app/tasks/page.tsx create mode 100644 frontend/src/ui/Input/Input.tsx create mode 100644 frontend/src/ui/Select/Select.tsx create mode 100644 frontend/src/ui/TextArea/TextArea.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 34e3ba155..4ff02af0e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.11.0", "@fluentui/react-icons": "^2.0.216", "@mui/joy": "^5.0.0-beta.6", + "@tanstack/react-query": "^4.35.3", "@types/node": "20.6.0", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", @@ -1944,6 +1945,41 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.35.3.tgz", + "integrity": "sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.35.3.tgz", + "integrity": "sha512-UgTPioip/rGG3EQilXfA2j4BJkhEQsR+KAbF+KIuvQ7j4MkgnTCJF01SfRpIRNtQTlEfz/+IL7+jP8WA8bFbsw==", + "dependencies": { + "@tanstack/query-core": "4.35.3", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@testing-library/dom": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", @@ -7983,6 +8019,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -9661,6 +9705,20 @@ "tslib": "^2.4.0" } }, + "@tanstack/query-core": { + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.35.3.tgz", + "integrity": "sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ==" + }, + "@tanstack/react-query": { + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.35.3.tgz", + "integrity": "sha512-UgTPioip/rGG3EQilXfA2j4BJkhEQsR+KAbF+KIuvQ7j4MkgnTCJF01SfRpIRNtQTlEfz/+IL7+jP8WA8bFbsw==", + "requires": { + "@tanstack/query-core": "4.35.3", + "use-sync-external-store": "^1.2.0" + } + }, "@testing-library/dom": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", @@ -14038,6 +14096,12 @@ "requires-port": "^1.0.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index f2d2f12b3..cecc260ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@emotion/styled": "^11.11.0", "@fluentui/react-icons": "^2.0.216", "@mui/joy": "^5.0.0-beta.6", + "@tanstack/react-query": "^4.35.3", "@types/node": "20.6.0", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 5b1259509..965cd8f27 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,78 +1,3 @@ -:root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', - 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', - 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } -} - * { box-sizing: border-box; padding: 0; @@ -87,13 +12,6 @@ body { body { display: flex; - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); @media (max-width: 600px) { flex-direction: column; @@ -104,9 +22,3 @@ a { color: inherit; text-decoration: none; } - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 320a03938..4dda8ba62 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,12 +1,10 @@ import './globals.css' import type { Metadata } from 'next' import localFont from 'next/font/local' -import { AuthProvider } from '@/app/auth/AuthProvider' import { Sidebar } from '@/ui/Sidebar/Sidebar' import { ContentSidebar } from '@/ui/ContentSidebar/ContentSidebar' -import { CssVarsProvider } from '@mui/joy/styles' -import { theme } from '@/ui/theme' import { Main, SkipNavigation } from '@/ui/SkipNavigation/SkipNavigation' +import { Providers } from './providers' const monaSans = localFont({ src: '../assets/fonts/Mona-Sans.woff2', @@ -22,18 +20,16 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - - - Skip Navigation - -
- {children} -
- -
Right Sidebar
-
-
-
+ + Skip Navigation + +
+ {children} +
+ +
Right Sidebar
+
+
) diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx new file mode 100644 index 000000000..421d3360c --- /dev/null +++ b/frontend/src/app/providers.tsx @@ -0,0 +1,18 @@ +'use client' + +import { CssVarsProvider } from '@mui/joy/styles' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { AuthProvider } from '@/app/auth/AuthProvider' +import { theme } from '@/ui/theme' + +const queryClient = new QueryClient() + +export const Providers = ({ children }: { children: React.ReactNode }) => { + return ( + + + {children} + + + ) +} diff --git a/frontend/src/app/tasks/hooks/useForm.ts b/frontend/src/app/tasks/hooks/useForm.ts new file mode 100644 index 000000000..f362191d0 --- /dev/null +++ b/frontend/src/app/tasks/hooks/useForm.ts @@ -0,0 +1,17 @@ +import { useState } from 'react' + +type UseFormProps = { + initialValues: T +} + +export const useForm = ({ initialValues }: UseFormProps) => { + const [formState, setFormState] = useState(initialValues) + + const handleChange = (field: F, newValue: T[F]) => { + setFormState((prevFormState) => ({ ...prevFormState, [field]: newValue })) + } + + const resetForm = () => setFormState(initialValues) + + return { handleChange, formState, resetForm } +} diff --git a/frontend/src/app/tasks/hooks/useProjects.ts b/frontend/src/app/tasks/hooks/useProjects.ts new file mode 100644 index 000000000..e7fd8428b --- /dev/null +++ b/frontend/src/app/tasks/hooks/useProjects.ts @@ -0,0 +1,39 @@ +import { useQuery } from '@tanstack/react-query' +import { useAuth } from 'react-oidc-context' + +import { apiClient } from '@/infra/apiClient' + +type Project = { + id: string + area_id: number + customer_id: number + description: string + is_active: boolean + init?: string + end?: string + invoice?: number + estimated_hours?: number + moved_hours?: number + project_type?: string + schedule_type?: string +} + +const fetchProjects = (token: string): Promise> => { + return apiClient(token) + .get('/v1/projects') + .then((response) => response.data) +} + +export const useProjects = () => { + const auth = useAuth() + const token = auth.user?.access_token || '' + + const { data = [], isLoading } = useQuery({ + queryKey: ['projects', token], + queryFn: () => { + return fetchProjects(token) + } + }) + + return { projects: data, isLoading } +} diff --git a/frontend/src/app/tasks/hooks/useTaskTypes.ts b/frontend/src/app/tasks/hooks/useTaskTypes.ts new file mode 100644 index 000000000..e8a555d9e --- /dev/null +++ b/frontend/src/app/tasks/hooks/useTaskTypes.ts @@ -0,0 +1,36 @@ +import { useQuery } from '@tanstack/react-query' +import { useAuth } from 'react-oidc-context' + +type TaskType = { + slug: string + name: string + active: boolean +} + +// Temporary disable so we don't need to change the fetch api for now. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const fetchTaskTypes = (token: string): Promise> => { + // return apiClient(token) + // .get('/v1/timelog/task_types/') + // .then((response) => response.data) + const mockData = Promise.resolve([ + { name: 'mock task type', slug: 'mock-test', active: true }, + { name: 'mock task type 2', slug: 'mock-test-2', active: true } + ]) + return mockData +} + +export const useTaskTypes = () => { + const auth = useAuth() + const token = auth.user?.access_token || '' + + const { data } = useQuery({ + queryKey: ['taskTypes', token], + queryFn: () => { + return fetchTaskTypes(token) + }, + initialData: [] + }) + + return data +} diff --git a/frontend/src/app/tasks/page.tsx b/frontend/src/app/tasks/page.tsx new file mode 100644 index 000000000..a8f0f9edd --- /dev/null +++ b/frontend/src/app/tasks/page.tsx @@ -0,0 +1,109 @@ +'use client' + +import Box from '@mui/joy/Box' +import Stack from '@mui/joy/Stack' +import Button from '@mui/joy/Button' +import { AuthorizedPage } from '@/app/auth/AuthorizedPage' +import { Select } from '@/ui/Select/Select' +import { Input } from '@/ui/Input/Input' +import { TextArea } from '@/ui/TextArea/TextArea' + +import { useProjects } from './hooks/useProjects' +import { useTaskTypes } from './hooks/useTaskTypes' +import { useForm } from './hooks/useForm' + +type Task = { + project: string + taskType: string + story: string + description: string +} + +export default function Tasks() { + const { projects, isLoading: isProjectsLoading } = useProjects() + const taskTypes = useTaskTypes() + + const { formState, handleChange, resetForm } = useForm({ + initialValues: { + project: '', + taskType: '', + story: '', + description: '' + } + }) + + return ( + + { + e.preventDefault() + console.log(formState) + }} + component="form" + maxWidth="558px" + margin="0 auto" + gap="16px" + > + + handleChange('story', e.target.value)} + name="story" + placeholder="Story" + label="Story" + /> + +