Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

158 link the contribution model to osbl creation #159

Merged
merged 4 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 70 additions & 58 deletions app/controllers/users/contributions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@ def new
end

def create
@osbl = Osbl.new(osbl_params)
osbl_params = contribution_params.delete(:osbl)
contribution = @user.contributions.build(contribution_params)
osbl = Osbl.new(osbl_params)

if @osbl.save
if osbl.valid?
osbl_data = OsblDataTransformer.new(osbl_params).transform
contribution.contributable = OsblCreation.new(osbl_data: osbl_data)

contribution.save!
redirect_to my_contributions_path, success: "Votre contribution a été enregistrée."
else
redirect_to my_new_contribution_path, inertia: {errors: @osbl.errors}
redirect_to my_new_contribution_path, inertia: {errors: osbl.errors}
end
end
mpressen marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -40,62 +46,68 @@ def get_user
@user = User.find(params[:user_id])
end

def osbl_params
params.permit(
:name,
:website,
:logo,
:description,
{osbls_causes_attributes: [:cause_id]},
:tax_reduction,
{osbls_keywords_attributes: [:keyword_id]},
:geographical_scale,
{osbls_intervention_areas_attributes: [:intervention_area_id]},
:osbl_type,
:public_utility,
:creation_year,
{osbls_labels_attributes: [:label_id]},
{annual_finances_attributes: [
:year,
:certified,
:budget,
:treasury,
:employees_count,
{fund_sources_attributes: [
:type,
:percent,
:amount
]},
{fund_allocations_attributes: [
:type,
:percent,
:amount
]}
]},
{document_attachments_attributes: [
{document_attributes: [
:type,
:file,
def contribution_params
@contribution_params ||= params.expect(
contribution: [
:body,
files: [],
osbl: [
:name,
:year,
:description
]}
]},
{locations_attributes: [
:type,
:name,
:description,
:website,
{address_attributes: [
:street_number,
:street_name,
:additional_info,
:postal_code,
:city,
:latitude,
:longitude
]}
]}
:website,
:logo,
:description,
:tax_reduction,
:geographical_scale,
:osbl_type,
:public_utility,
:creation_year,
osbls_causes_attributes: [[:cause_id]],
osbls_keywords_attributes: [[:keyword_id]],
osbls_intervention_areas_attributes: [[:intervention_area_id]],
osbls_labels_attributes: [[:label_id]],
annual_finances_attributes: [[
:year,
:certified,
:budget,
:treasury,
:employees_count,
fund_sources_attributes: [[
:type,
:percent,
:amount
]],
fund_allocations_attributes: [[
:type,
:percent,
:amount
]]
]],
document_attachments_attributes: [[
document_attributes: [
:type,
:file,
:name,
:year,
:description
]
]],
locations_attributes: [[
:type,
:name,
:description,
:website,
address_attributes: [
:street_number,
:street_name,
:additional_info,
:postal_code,
:city,
:latitude,
:longitude
]
]]
]
]
)
mpressen marked this conversation as resolved.
Show resolved Hide resolved
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function OsblHeader ({ data, setData, errors, clearErrors }: Omit

<MyFileInput
id='logo'
accept='image/png, image/svg, image/webp'
accept='image/png, image/svg+xml, image/webp'
labelText={
<p className='flex items-center gap-2'>
Logo
Expand Down
135 changes: 97 additions & 38 deletions app/frontend/pages/Contribution/New.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Head, useForm } from '@inertiajs/react'
import { ReactElement } from 'react'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { ReactElement, useState, useRef, useEffect } from 'react'
// import { Alert, AlertDescription } from '@/components/ui/alert'
// @ts-expect-error
import GoodIdea from '@/assets/icons/good-idea.svg?react'
// import GoodIdea from '@/assets/icons/good-idea.svg?react'
import { Button } from '@/components/ui/button'
import { Save } from 'lucide-react'
import OsblHeader from '@/components/pages/contribution/new/OsblHeader'
Expand All @@ -11,42 +11,99 @@ import OsblFinance from '@/components/pages/contribution/new/OsblFinances'
import OsblDocuments from '@/components/pages/contribution/new/OsblDocuments'
import OsblLocations from '@/components/pages/contribution/new/OsblLocations'
import { CurrentUserType } from '@/types/types'
import { FormData } from '@/pages/Contribution/types'
import { OsblCreationData, FormProps } from '@/pages/Contribution/types'
import z from 'zod'
import deepCleanData from '@/lib/deepCleanData'
import { toast } from 'sonner'
import { FormDataConvertible } from '@inertiajs/core'

const MAX_LOGO_SIZE = 1 * 1024 * 1024 // 1MB
const ALLOWED_LOGO_TYPES = ['image/svg', 'image/png', 'image/webp']
const ALLOWED_LOGO_TYPES = ['image/svg+xml', 'image/png', 'image/webp']

const validation = z.object({
website: z.string().url({ message: 'Veuillez entrer une URL valide.' }).optional(),
logo: z.instanceof(File)
.refine((file) => {
return file.size <= MAX_LOGO_SIZE
}, 'La taille du fichier doit être inférieure à 1 MB.')
.refine((file) => {
return ALLOWED_LOGO_TYPES.includes(file.type)
}, 'Le type de fichier est invalide. Format accepté : SVG, PNG, WEBP.')
.optional(),
osbls_causes_attributes: z.array(z.object({ cause_id: z.string() })).min(1, { message: 'Au moins une cause est requise.' }),
tax_reduction: z.enum(['intérêt_général', 'aide_aux_personnes_en_difficulté'], { message: 'Veuillez sélectionner un pourcentage.' })
contribution: z.object({
osbl: z.object({
website: z.string().url({ message: 'Veuillez entrer une URL valide.' }).optional(),
logo: z.instanceof(File)
.refine((file) => {
return file.size <= MAX_LOGO_SIZE
}, 'La taille du fichier doit être inférieure à 1 MB.')
.refine((file) => {
return ALLOWED_LOGO_TYPES.includes(file.type)
}, 'Le type de fichier est invalide. Format accepté : SVG, PNG, WEBP.')
.optional(),
osbls_causes_attributes: z.array(z.object({ cause_id: z.string() })).min(1, { message: 'Au moins une cause est requise.' }),
tax_reduction: z.enum(['intérêt_général', 'aide_aux_personnes_en_difficulté'], { message: 'Veuillez sélectionner un pourcentage.' })
})
})
})

type StrictForm<T> = T & {
[K in keyof T]: T[K] extends FormDataConvertible ? T[K] : never
} & {
[key: string]: FormDataConvertible
function createOsblProxy (
data: OsblCreationData,
setData: (key: string, value: any) => void,
errors: Record<string, string>,
clearErrors: (field: 'contribution') => void,
setError: (field: 'contribution', message: string) => void
): FormProps {
return {
data: data.contribution.osbl,
setData: (key, value) => {
const updatedOsbl = {
...data.contribution.osbl,
[key]: value
}

setData('contribution', {
...data.contribution,
osbl: updatedOsbl
})
},
errors: Object.entries(errors).reduce<Record<string, string>>((acc, [key, value]) => {
const match = key.match(/^contribution\.osbl\.(.+)/)
if (match != null) {
acc[match[1]] = value
}
return acc
}, {}),
clearErrors: (field: string) => clearErrors(`contribution.osbl.${field}` as 'contribution'),
setError: (field, message) => setError(`contribution.osbl.${field}` as 'contribution', message)
}
}

export default function New ({ currentUser }: { currentUser: CurrentUserType }): ReactElement {
const { data, setData, post, processing, errors, clearErrors, setError, transform } = useForm<StrictForm<FormData>>({
name: '',
osbls_causes_attributes: [],
tax_reduction: ''
// inertia types don't handle nested data properly.
const { data, setData, post, processing, errors, clearErrors, setError, transform } = useForm({
contribution: {
osbl: {
name: '',
osbls_causes_attributes: [],
tax_reduction: ''
}
}
})

// Create a proxy for the form props
const formProps = createOsblProxy(data, setData, errors, clearErrors, setError)

// Add state for button visibility
const [showBottomButton, setShowBottomButton] = useState(false)
const topButtonRef = useRef<HTMLButtonElement>(null)

// Add intersection observer
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setShowBottomButton(!entry.isIntersecting)
},
{ threshold: 1 }
)

if (topButtonRef.current != null) {
observer.observe(topButtonRef.current)
}

return () => observer.disconnect()
}, [])

function submit (e: React.FormEvent<HTMLFormElement>): void {
e.preventDefault()

Expand All @@ -55,7 +112,7 @@ export default function New ({ currentUser }: { currentUser: CurrentUserType }):
if (!result.success) {
const issues = result.error.issues
issues.forEach(issue => {
setError(issue.path.join('.') as keyof FormData, issue.message)
setError(issue.path.join('.') as 'contribution', issue.message)
})

toast.error('Veuillez corriger les erreurs avant de continuer.')
Expand All @@ -78,35 +135,37 @@ export default function New ({ currentUser }: { currentUser: CurrentUserType }):
<form onKeyDown={avoidUnintentionalSubmission} onSubmit={submit} className='2xl:container mx-auto flex flex-col px-2 sm:px-8 md:px-16 pt-8 pb-16 gap-8'>
<div className='flex gap-8 sm:gap-16 items-center flex-wrap justify-center md:justify-start'>
<h1 className='font-semibold text-2xl sm:text-3xl'>Ajouter une association</h1>
<Button type='submit' disabled={processing} className='text-lg'>
<Button ref={topButtonRef} type='submit' disabled={processing} className='text-lg'>
<Save className='mr-2' />
Enregistrer
</Button>
</div>
<Alert>
{/* <Alert>
<GoodIdea className='min-w-8 min-h-8' />
<AlertDescription>
Pour que votre contribution soit validée, le modérateur doit pouvoir
vérifier les informations fournies. Facilitez son travail en
indiquant clairement vos sources !
</AlertDescription>
</Alert>
</Alert> */}

<div className='flex flex-col pt-4 gap-8'>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-8'>
<OsblHeader data={data} setData={setData} errors={errors} clearErrors={clearErrors} />
<OsblDataSheet data={data} setData={setData} errors={errors} clearErrors={clearErrors} />
<OsblHeader {...formProps} />
<OsblDataSheet {...formProps} />
</div>
<div className='grid grid-cols-1 lg:grid-cols-3 gap-8'>
<OsblFinance data={data} setData={setData} errors={errors} clearErrors={clearErrors} setError={(field, message) => setError(field as keyof FormData, message)} />
<OsblDocuments data={data} setData={setData} errors={errors} clearErrors={clearErrors} setError={(field, message) => setError(field as keyof FormData, message)} />
<OsblLocations data={data} setData={setData} />
<OsblFinance {...formProps} />
<OsblDocuments {...formProps} />
<OsblLocations {...formProps} />
</div>
</div>
<Button type='submit' disabled={processing} className='text-lg ml-auto mt-4 sm:mt-8'>
<Save className='mr-2' />
Enregistrer
</Button>
{showBottomButton && (
<Button type='submit' disabled={processing} className='text-lg ml-auto mt-4 sm:mt-8'>
<Save className='mr-2' />
Enregistrer
</Button>
)}
</form>
</>
)
Expand Down
18 changes: 13 additions & 5 deletions app/frontend/pages/Contribution/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@ export interface Location {
website?: string
}

export interface FormData {
export interface OsblCreationData {
contribution: {
body?: string
files?: File[]
osbl: OsblData
}
}

export interface OsblData {
name: string
website?: string
logo?: File
Expand All @@ -68,9 +76,9 @@ export interface FormData {
}

export interface FormProps {
data: FormData
setData: (key: keyof FormData | string, value: FormData[keyof FormData] | any) => void
errors: Partial<Record<keyof FormData | string, string>>
clearErrors: (...fields: Array<keyof FormData | any>) => void
data: OsblData
setData: (key: keyof OsblData | string, value: OsblData[keyof OsblData] | any) => void
errors: Partial<Record<keyof OsblData | string, string>>
clearErrors: (...fields: Array<keyof OsblData | string>) => void
setError: (field: string, error: any) => void
}
2 changes: 1 addition & 1 deletion app/frontend/tests/forms/SignUpForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'
import { forwardRef, useImperativeHandle } from 'react'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import SignUpForm from '@/components/forms/SignUpForm'
import SignUpForm from '@/components/pages/auth/signUp/SignUpForm'
import useFormHandler from '@/hooks/useFormHandler'

vi.mock('@inertiajs/react', async () => {
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/tests/pages/auth/SignIn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ vi.mock('@inertiajs/react', async () => {
}
})

vi.mock('@/components/forms/SignInForm', () => ({
vi.mock('@/components/pages/auth/signIn/SignInForm', () => ({
__esModule: true,
default: vi.fn(() => <div data-testid='sign-in-form' />)
}))
Expand Down
Loading