diff --git a/src/application/helpers.js b/src/application/helpers.js index 892c1ee36..c1b2753a3 100644 --- a/src/application/helpers.js +++ b/src/application/helpers.js @@ -361,6 +361,8 @@ export const getInitialApplicationForm = ( }; export const getApplicationAttachmentDownloadLink = (id: number): string => createUrl(`attachment/${id}/download`); +export const getAreaSearchApplicationAttachmentDownloadLink = (id: number): string => createUrl(`area_search_attachment/${id}/download`); + export const getSectionTemplate = (formName: string, formPath: string, identifier: string): Object => { const state = store.getState(); const templates = formValueSelector(formName)( diff --git a/src/areaSearch/actions.js b/src/areaSearch/actions.js index 353b906d9..ce7fbaae2 100644 --- a/src/areaSearch/actions.js +++ b/src/areaSearch/actions.js @@ -41,6 +41,8 @@ import type { UploadAreaSearchAttachmentAction, ReceiveFileOperationFinishedAction, ReceiveFileOperationFailedAction, + SetAreaSearchAttachmentsAction, + UploadedAreaSearchAttachmentMeta, } from '$src/areaSearch/types'; import type {Attributes, Methods} from '$src/types'; @@ -160,3 +162,6 @@ export const receiveFileOperationFinished = (): ReceiveFileOperationFinishedActi export const receiveFileOperationFailed = (error: any): ReceiveFileOperationFailedAction => createAction('mvj/areaSearch/RECEIVE_FILE_OPERATION_FAILED')(error); + +export const setAreaSearchAttachments = (attachments: Array): SetAreaSearchAttachmentsAction => + createAction('mvj/areaSearch/SET_ATTACHMENTS')(attachments); \ No newline at end of file diff --git a/src/areaSearch/components/AreaSearchApplication.js b/src/areaSearch/components/AreaSearchApplication.js index e5a7a3dcc..93494baa8 100644 --- a/src/areaSearch/components/AreaSearchApplication.js +++ b/src/areaSearch/components/AreaSearchApplication.js @@ -18,7 +18,10 @@ import { } from '$util/helpers'; import type {Attributes} from '$src/types'; import {reshapeSavedApplicationObject} from '$src/plotApplications/helpers'; -import {transformApplicantSectionTitle} from '$src/application/helpers'; +import { + transformApplicantSectionTitle, + getAreaSearchApplicationAttachmentDownloadLink, +} from '$src/application/helpers'; import Title from '$components/content/Title'; import Divider from '$components/content/Divider'; import Collapse from '$components/collapse/Collapse'; @@ -212,15 +215,16 @@ class AreaSearchApplication extends Component { ]} />)} - {areaSearch.area_search_attachments.map((file, index) => - Liite {index + 1} - - - - )} + {areaSearch.area_search_attachments.map((file, index) => { + return ( + Liite {index + 1} + + + + )})} {areaSearch.area_search_attachments.length === 0 &&

Hakemuksella ei ole liitteitä.

} diff --git a/src/areaSearch/components/AreaSearchApplicationEdit.js b/src/areaSearch/components/AreaSearchApplicationEdit.js index 2e1221f03..9b87ff23b 100644 --- a/src/areaSearch/components/AreaSearchApplicationEdit.js +++ b/src/areaSearch/components/AreaSearchApplicationEdit.js @@ -1,5 +1,4 @@ // @flow - import React, {Component, Fragment} from 'react'; import {connect} from 'react-redux'; import flowRight from 'lodash/flowRight'; @@ -17,10 +16,14 @@ import { getFieldAttributes, getFieldOptions, getLabelOfOption, + displayUIMessage, } from '$util/helpers'; import type {Attributes} from '$src/types'; import {reshapeSavedApplicationObject} from '$src/plotApplications/helpers'; -import {transformApplicantSectionTitle} from '$src/application/helpers'; +import { + transformApplicantSectionTitle, + getAreaSearchApplicationAttachmentDownloadLink, +} from '$src/application/helpers'; import Title from '$components/content/Title'; import Divider from '$components/content/Divider'; import Collapse from '$components/collapse/Collapse'; @@ -41,21 +44,24 @@ import {getInitialAreaSearchEditForm, transformApplicantInfoCheckTitle} from '$s import FormField from '$components/form/FormField'; import TitleH3 from '$components/content/TitleH3'; import AreaSearchStatusNoteHistory from '$src/areaSearch/components/AreaSearchStatusNoteHistory'; -import {getFormAttributes, getIsFetchingFormAttributes} from '$src/application/selectors'; +import {getFormAttributes, getIsFetchingFormAttributes, getIsPerformingFileOperation} from '$src/application/selectors'; import {APPLICANT_SECTION_IDENTIFIER} from '$src/application/constants'; import type {Form} from '$src/application/types'; +import type {AreaSearch, UploadedAreaSearchAttachmentMeta} from '$src/areaSearch/types'; +import AddFileButton from '$components/form/AddFileButton'; +import {uploadAttachment, setAreaSearchAttachments} from '$src/areaSearch/actions'; +import RemoveButton from '$components/form/RemoveButton'; -type OwnProps = { - -}; type Props = { - ...OwnProps, - areaSearch: Object | null, + areaSearch: AreaSearch | null, isFetchingFormAttributes: boolean, + isPerformingFileOperation: boolean, formAttributes: Attributes, areaSearchAttributes: Attributes, initialize: Function, + uploadAttachment: Function, + setAreaSearchAttachments: Function, }; type State = { @@ -91,14 +97,39 @@ class AreaSearchApplicationEdit extends Component { initialize(getInitialAreaSearchEditForm(areaSearch)); } + + handleFileAdded = (e: Event) => { + const {uploadAttachment, areaSearch, setAreaSearchAttachments} = this.props; + const currentFiles = areaSearch?.area_search_attachments || []; + const file = e.target.files[0]; + const fileExists = currentFiles.find((currentFile) => currentFile.name === file.name); + + if (fileExists) { + displayUIMessage({title: 'Virhe', body: `Tiedosto nimellä ${file.name} on jo listassa.`}, {type: 'error'}); + } else { + uploadAttachment({ + fileData: file, + areaSearch: areaSearch?.id, + callback: (newFile: UploadedAreaSearchAttachmentMeta) => { + setAreaSearchAttachments([...currentFiles, newFile]) + }, + }); + } + + } + render(): React$Node { const { areaSearch, isFetchingFormAttributes, + isPerformingFileOperation, formAttributes, areaSearchAttributes, + attachments, + addAttachment, } = this.props; + const { selectedAreaSectionRefreshKey, } = this.state; @@ -249,18 +280,25 @@ class AreaSearchApplicationEdit extends Component { ]} />)} - {areaSearch.area_search_attachments.map((file, index) => + {areaSearch.area_search_attachments.map((file, index) => { + return ( Liite {index + 1} - + - )} + )})} {areaSearch.area_search_attachments.length === 0 &&

Hakemuksella ei ole liitteitä.

} +
@@ -352,13 +390,20 @@ class AreaSearchApplicationEdit extends Component { } } + export default (flowRight( connect((state) => ({ areaSearch: getCurrentAreaSearch(state), areaSearchAttributes: getAttributes(state), formAttributes: getFormAttributes(state), isFetchingFormAttributes: getIsFetchingFormAttributes(state), - })), + isPerformingFileOperation: getIsPerformingFileOperation(state), + }), + { + uploadAttachment, + setAreaSearchAttachments, + }, + ), reduxForm({ form: FormNames.AREA_SEARCH, }), diff --git a/src/areaSearch/components/AreaSearchApplicationListPage.js b/src/areaSearch/components/AreaSearchApplicationListPage.js index c964e71b7..435872835 100644 --- a/src/areaSearch/components/AreaSearchApplicationListPage.js +++ b/src/areaSearch/components/AreaSearchApplicationListPage.js @@ -69,8 +69,8 @@ import type {Attributes, Methods as MethodsType} from '$src/types'; import type {ApiResponse} from '$src/types'; import type {UsersPermissions as UsersPermissionsType} from '$src/usersPermissions/types'; import AreaSearchExportModal from '$src/areaSearch/components/AreaSearchExportModal'; -import { getUserActiveServiceUnit } from "../../usersPermissions/selectors"; -import type { UserServiceUnit } from "../../usersPermissions/types"; +import {getUserActiveServiceUnit} from '$src/usersPermissions/selectors'; +import type {UserServiceUnit} from '$src/usersPermissions/types'; const VisualizationTypes = { MAP: 'map', diff --git a/src/areaSearch/components/AreaSearchApplicationPage.js b/src/areaSearch/components/AreaSearchApplicationPage.js index 7aebdce05..b6db47d2a 100644 --- a/src/areaSearch/components/AreaSearchApplicationPage.js +++ b/src/areaSearch/components/AreaSearchApplicationPage.js @@ -302,16 +302,6 @@ class AreaSearchApplicationPage extends Component { scrollToTopPage(); } - /* - if (isEmpty(prevProps.currentAreaSearch) && !isEmpty(currentAreaSearch)) { - const storedAreaSearchId = getSessionStorageItem('areaSearchId'); - - if(Number(areaSearchId) === storedAreaSearchId) { - this.setState({isRestoreModalOpen: true}); - } - } - */ - if (!isFetching && prevProps.isFetching) { setPageTitle(`Hakemus ${currentAreaSearch?.identifier || ''}`); } diff --git a/src/areaSearch/reducer.js b/src/areaSearch/reducer.js index 9ad615cf6..edbb3527b 100644 --- a/src/areaSearch/reducer.js +++ b/src/areaSearch/reducer.js @@ -10,7 +10,11 @@ import type { ReceiveFormValidFlagsAction, ReceiveIsSaveClickedAction, ReceiveSingleAreaSearchAction, } from '$src/areaSearch/types'; -import type {ReceiveAttributesAction, ReceiveMethodsAction} from '$src/areaSearch/types'; +import type { + ReceiveAttributesAction, + ReceiveMethodsAction, + SetAreaSearchAttachmentsAction +} from '$src/areaSearch/types'; import type {ApiResponse} from '$src/types'; import merge from 'lodash/merge'; @@ -82,6 +86,13 @@ const currentAreaSearchReducer: Reducer = handleActions({ }, ['mvj/areaSearch/CREATE_SPECS']: () => null, ['mvj/areaSearch/RECEIVE_SPECS_CREATED']: (state, {payload: result}) => result, + ['mvj/areaSearch/SET_ATTACHMENTS']: (state, {payload: attachments}) => { + const { area_search_attachments } = state; + return { + ...state, + area_search_attachments: attachments + } + }, }, null); const isFetchingCurrentAreaSearchReducer: Reducer = handleActions({ diff --git a/src/areaSearch/spec.js b/src/areaSearch/spec.js index 5ea6af339..a4c65246f 100644 --- a/src/areaSearch/spec.js +++ b/src/areaSearch/spec.js @@ -25,6 +25,7 @@ import { receiveListMethods, receiveMethods, receiveSingleAreaSearch, + setAreaSearchAttachments, showEditMode, singleAreaSearchNotFound, } from '$src/areaSearch/actions'; @@ -275,6 +276,31 @@ describe('AreaSearch', () => { }, receiveAreaSearchEditFailed('test error')); expect(state).to.deep.equal(newState); }); + + it('should update area search attachments', () => { + const dummyFile = { + id: 1, + attachment: "http://localhost:8001/media/area_search_attachments/2024-04-19/filename.pdf", + name: "filename.pdf", + field: 1, + created_at: "2024-04-19T10:54:21.269517+03:00", + user: { + id: 1, + first_name: "Matti", + last_name: "Meikäläinen", + is_staff: false, + username: "u-abcdefg1hij23klmnvwxyzabcd" + } + }; + const newState = {...defaultState, currentAreaSearch: {area_search_attachments: [dummyFile]}}; + + const state = areaSearchReducer({ + currentAreaSearch: { + area_search_attachments: [], + }, + }, setAreaSearchAttachments([dummyFile])); + expect(state).to.deep.equal(newState); + }); }); }); }); diff --git a/src/areaSearch/types.js b/src/areaSearch/types.js index 6f044a4ab..742395dee 100644 --- a/src/areaSearch/types.js +++ b/src/areaSearch/types.js @@ -1,5 +1,5 @@ // @flow -import type {Action, ApiResponse, Attributes, Methods} from '$src/types'; +import type {Action, ApiResponse, Attributes, Methods, User} from '$src/types'; import type {UploadedFileMeta} from '$src/application/types'; export type AreaSearchState = { @@ -27,9 +27,42 @@ export type AreaSearchState = { isPerformingFileOperation: boolean, }; -export type AreaSearch = Object; +export type AreaSearch = { + id: number; + address?: string; + answer: Object; // TODO: specify + applicants: Array; + area_search_attachments: Array; + area_search_status: Object; // TODO: specify + description_area?: string; + description_intended_use?: string; + district: string; + end_date?: string; + form: Object; // TODO: specify + geometry: Object; // TODO: specify + identifier: string; + intended_use: number; + lessor: string; + plot: Array; + preparer?: User; + received_date?: string; + service_unit?: number; + start_date: string; + state: string; +}; + export type AreaSearchId = number; +export type UploadedAreaSearchAttachmentMeta = { + id: number; + attachment: string; + name: string; + field: number; + created_at: string; + user?: User; +}; + + export type FetchListAttributesAction = Action<'mvj/areaSearch/FETCH_LIST_ATTRIBUTES', void>; export type ReceiveListAttributesAction = Action<'mvj/areaSearch/RECEIVE_LIST_ATTRIBUTES', Attributes>; export type ReceiveListMethodsAction = Action<'mvj/areaSearch/RECEIVE_LIST_METHODS', Methods>; @@ -74,6 +107,7 @@ export type ReceiveAreaSearchInfoCheckBatchEditFailureAction = Action<'mvj/areaS export type EditAreaSearchAction = Action<'mvj/areaSearch/EDIT', Object>; export type ReceiveAreaSearchEditedAction = Action<'mvj/areaSearch/RECEIVE_EDITED', void>; export type ReceiveAreaSearchEditFailedAction = Action<'mvj/areaSearch/RECEIVE_EDIT_FAILED', Object>; +export type SetAreaSearchAttachmentsAction = Action<'mvj/areaSearch/SET_ATTACHMENTS', Array>; export type CreateAreaSearchSpecsAction = Action<'mvj/areaSearch/CREATE_SPECS', Object>; export type ReceiveAreaSearchSpecsCreatedAction = Action<'mvj/areaSearch/RECEIVE_SPECS_CREATED', Object>; @@ -88,8 +122,8 @@ export type DeleteAreaSearchAttachmentAction = Action<'mvj/areaSearch/DELETE_ATT callback?: () => void, }>; export type UploadAreaSearchAttachmentAction = Action<'mvj/areaSearch/UPLOAD_ATTACHMENT', { - fileData: Object, - callback?: (fileData: UploadedFileMeta) => void, + fileData: File, + callback?: (fileData: UploadedAreaSearchAttachmentMeta) => void, areaSearch?: number, }>; export type ReceiveFileOperationFinishedAction = Action<'mvj/areaSearch/RECEIVE_FILE_OPERATION_FINISHED', void>; diff --git a/src/types.js b/src/types.js index ad58eed4f..cfdc67200 100644 --- a/src/types.js +++ b/src/types.js @@ -41,3 +41,11 @@ export type LeafletGeoJson = { features: Array, type: 'FeatureCollection', } + +export type User = { + id: number; + first_name?: string; + last_name?: string; + is_staff: boolean; + username: string; +} \ No newline at end of file