Skip to content

Commit

Permalink
Merge branch 'development' of github.com:hotosm/fmtm into fix/task-co…
Browse files Browse the repository at this point in the history
…mment-events
  • Loading branch information
NSUWAL123 committed Nov 12, 2024
2 parents a08d931 + 48b55db commit 21686be
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 30 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
repos:
# Versioning: Commit messages & changelog
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.30.0
rev: v3.30.1
hooks:
- id: commitizen
stages: [commit-msg]

# Lint / autoformat: Python code
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: "v0.7.2"
rev: "v0.7.3"
hooks:
# Run the linter
- id: ruff
Expand Down
15 changes: 0 additions & 15 deletions src/backend/app/auth/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ async def check_access(
Access is determined based on the user's role and permissions:
- If the user has an 'ADMIN' role, access is granted.
- If the user has a 'READ_ONLY' role, access is denied.
- If the organisation is HOTOSM, then grant access.
- For other roles, access is granted if the user is an organisation manager
for the specified organisation (org_id) or has the specified role
in the specified project (project_id).
Expand All @@ -92,20 +91,6 @@ async def check_access(
CASE
WHEN role = 'ADMIN' THEN true
WHEN role = 'READ_ONLY' THEN false
WHEN EXISTS (
SELECT 1
FROM organisations
WHERE (organisations.id = %(org_id)s
AND organisations.slug = 'hotosm')
OR EXISTS (
SELECT 1
FROM projects
JOIN organisations AS org
ON projects.organisation_id = org.id
WHERE org.slug = 'hotosm'
AND projects.id = %(project_id)s
)
) THEN true
ELSE
EXISTS (
SELECT 1
Expand Down
4 changes: 3 additions & 1 deletion src/frontend/src/api/CreateProjectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ const GenerateProjectFilesService = (url: string, projectData: any, formUpload:
try {
let response;
const additional_entities: string[] =
projectData?.additional_entities?.length > 0 ? [projectData?.additional_entities?.[0].replace(' ', '_')] : [];
projectData?.additional_entities?.length > 0
? [projectData?.additional_entities?.[0]?.replaceAll(' ', '_')]
: [];
if (projectData.form_ways === 'custom_form') {
// TODO move form upload to a separate service / endpoint?
const generateApiFormData = new FormData();
Expand Down
18 changes: 18 additions & 0 deletions src/frontend/src/api/Submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,21 @@ export const SubmissionService: Function = (url: string) => {
await getSubmissionDetails(url);
};
};

export const GetSubmissionPhotosService: Function = (url: string) => {
return async (dispatch) => {
dispatch(SubmissionActions.SetSubmissionPhotosLoading(true));
const getSubmissionPhotos = async (url: string) => {
try {
const response = await axios.get(url);
dispatch(SubmissionActions.SetSubmissionPhotos(response?.data?.image_urls));
dispatch(SubmissionActions.SetSubmissionPhotosLoading(false));
} catch (error) {
dispatch(SubmissionActions.SetSubmissionPhotosLoading(false));
} finally {
dispatch(SubmissionActions.SetSubmissionPhotosLoading(false));
}
};
await getSubmissionPhotos(url);
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import CoreModules from '@/shared/CoreModules';
import { FormCategoryService } from '@/api/CreateProjectService';
import { DownloadProjectForm } from '@/api/Project';
import { PostFormUpdate } from '@/api/CreateProjectService';
import { CreateProjectActions } from '@/store/slices/CreateProjectSlice';
import useDocumentTitle from '@/utilfunctions/useDocumentTitle';

type FileType = {
Expand All @@ -24,7 +23,7 @@ const FormUpdateTab = ({ projectId }) => {
const [uploadForm, setUploadForm] = useState<FileType[] | null>(null);
const [error, setError] = useState({ formError: '', categoryError: '' });

const xFormId = CoreModules.useAppSelector((state) => state.project.projectInfo.odk_form_id);
const xFormId = CoreModules.useAppSelector((state) => state.createproject.editProjectDetails.odk_form_id);
const formCategoryList = useAppSelector((state) => state.createproject.formCategoryList);
const sortedFormCategoryList = formCategoryList.slice().sort((a, b) => a.title.localeCompare(b.title));
const selectedCategory = useAppSelector((state) => state.createproject.editProjectDetails.xform_category);
Expand Down
23 changes: 22 additions & 1 deletion src/frontend/src/components/ProjectDetailsV2/ProjectOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AssetModules from '@/shared/AssetModules';
import { DownloadDataExtract, DownloadProjectForm, DownloadSubmissionGeojson } from '@/api/Project';
import Button from '@/components/common/Button';
import { useAppSelector } from '@/types/reduxTypes';
import { GetProjectQrCode } from '@/api/Files';

type projectOptionPropTypes = {
projectName: string;
Expand All @@ -19,7 +20,11 @@ const ProjectOptions = ({ projectName }: projectOptionPropTypes) => {

const projectId: string = params.id;

const handleDownload = (downloadType: 'form' | 'geojson' | 'extract' | 'submission') => {
const odkToken = useAppSelector((state) => state.project.projectInfo.odk_token);
const authDetails = CoreModules.useAppSelector((state) => state.login.authDetails);
const { qrcode }: { qrcode: string } = GetProjectQrCode(odkToken, projectName, authDetails?.username);

const handleDownload = (downloadType: 'form' | 'geojson' | 'extract' | 'submission' | 'qr') => {
if (downloadType === 'form') {
dispatch(
DownloadProjectForm(
Expand Down Expand Up @@ -50,6 +55,11 @@ const ProjectOptions = ({ projectName }: projectOptionPropTypes) => {
projectName,
),
);
} else if (downloadType === 'qr') {
const downloadLink = document.createElement('a');
downloadLink.href = qrcode;
downloadLink.download = `Project_${projectId}`;
downloadLink.click();
}
};

Expand Down Expand Up @@ -116,6 +126,17 @@ const ProjectOptions = ({ projectName }: projectOptionPropTypes) => {
handleDownload('submission');
}}
/>
<Button
loadingText="QR CODE"
btnText="QR CODE"
btnType="other"
className={`fmtm-border-red-700 !fmtm-rounded-md fmtm-truncate`}
icon={<AssetModules.FileDownloadIcon style={{ fontSize: '22px' }} />}
onClick={(e) => {
e.stopPropagation();
handleDownload('qr');
}}
/>
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const TaskSelectionPopup = ({ taskId, body, feature }: TaskSelectionPopupPropTyp
{/* only display qr code component render inside taskPopup on mobile screen */}
<div className="sm:fmtm-hidden">
{checkIfTaskAssignedOrNot && task_state !== 'LOCKED_FOR_MAPPING' && (
<QrcodeComponent projectId={currentProjectId} taskIndex={selectedTask.index} />
<QrcodeComponent projectId={currentProjectId} />
)}
</div>
{body}
Expand Down
5 changes: 2 additions & 3 deletions src/frontend/src/components/QrcodeComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import { GetProjectQrCode } from '@/api/Files';

type tasksComponentType = {
projectId?: string;
taskIndex?: number;
};

const QrcodeComponent = ({ projectId, taskIndex }: tasksComponentType) => {
const QrcodeComponent = ({ projectId }: tasksComponentType) => {
const downloadQR = () => {
const downloadLink = document.createElement('a');
downloadLink.href = qrcode;
downloadLink.download = `Project_${projectId}_Task_${taskIndex}`;
downloadLink.download = `Project_${projectId}`;
downloadLink.click();
};

Expand Down
109 changes: 109 additions & 0 deletions src/frontend/src/components/common/ImageSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useEffect, useRef, useState } from 'react';
import AssetModules from '@/shared/AssetModules';
import { Dialog, DialogContent } from '@/components/common/Modal';

type ImageSliderProps = {
images: string[];
};

const ImageSlider = ({ images }: ImageSliderProps) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);

const [isOverflowing, setIsOverflowing] = useState(false);
const [showRightButton, setShowRightButton] = useState(false);
const [showLeftButton, setShowLeftButton] = useState(false);
const [selectedImageURL, setSelectedImageURL] = useState<string | null>(null);
const [translateImage, setTranslateImage] = useState(0);

useEffect(() => {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
setIsOverflowing(container.scrollWidth > container.clientWidth);
}
}, [images]);

const handleScroll = () => {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
const atStart = container.scrollLeft === 0;
const atEnd = container.scrollLeft + container.clientWidth >= container.scrollWidth - 1;
setShowLeftButton(!atStart);
setShowRightButton(!atEnd);
}
};

return (
<>
<Dialog
open={selectedImageURL ? true : false}
onOpenChange={(status) => {
if (!status) {
setSelectedImageURL(null);
setTranslateImage(0);
}
}}
>
<DialogContent className="!fmtm-bg-transparent fmtm-border-none fmtm-shadow-none fmtm-h-[100vh] fmtm-w-[100vw]">
<div className="fmtm-z-50 fmtm-relative fmtm-overflow-hidden fmtm-p-4 fmtm-w-full fmtm-flex fmtm-justify-center">
<img
src={selectedImageURL || ''}
alt="submission"
className="fmtm-max-w-[95%] fmtm-max-h-[80vh] fmtm-object-cover"
style={{
transform: `rotate(${translateImage}deg)`,
}}
/>
</div>
<div className="fmtm-flex fmtm-items-center fmtm-gap-4 fmtm-px-4 fmtm-py-2 fmtm-absolute fmtm-bottom-5 fmtm-w-fit fmtm-bg-black fmtm-bg-opacity-30 fmtm-rounded-md fmtm-z-50 fmtm-left-[50%] fmtm-translate-x-[-50%]">
<AssetModules.RotateLeftIcon
className="fmtm-text-white fmtm-cursor-pointer hover:fmtm-scale-110 fmtm-duration-150"
onClick={() => setTranslateImage(translateImage - 90)}
/>
<AssetModules.RotateRightIcon
className="fmtm-text-white fmtm-cursor-pointer hover:fmtm-scale-110 fmtm-duration-150"
onClick={() => setTranslateImage(translateImage + 90)}
/>
</div>
</DialogContent>
</Dialog>
<div
className="fmtm-flex fmtm-gap-x-3 fmtm-w-full fmtm-overflow-x-scroll scrollbar fmtm-relative"
ref={scrollContainerRef}
onScroll={handleScroll}
>
{showLeftButton && (
<button
className={`fmtm-sticky fmtm-left-2 fmtm-my-auto fmtm-z-50 fmtm-w-fit fmtm-p-1 fmtm-rounded-full fmtm-bg-black fmtm-bg-opacity-50 fmtm-h-fit hover:fmtm-scale-110 fmtm-cursor-pointer fmtm-duration-300`}
onClick={() => {
scrollContainerRef?.current?.scrollBy({ left: -300, behavior: 'smooth' });
}}
>
<AssetModules.ChevronLeftIcon className="fmtm-text-white" />
</button>
)}
{images?.map((imageUrl, index) => (
<div
key={index}
onClick={() => setSelectedImageURL(imageUrl)}
className="fmtm-h-[10.313rem] fmtm-w-[9.688rem] fmtm-min-w-[9.688rem] fmtm-rounded-lg fmtm-overflow-hidden fmtm-cursor-pointer"
>
<img src={imageUrl} alt="submission image" className="fmtm-h-full fmtm-w-full fmtm-object-cover" />
</div>
))}

{((isOverflowing && showRightButton) || (isOverflowing && scrollContainerRef?.current?.scrollLeft === 0)) && (
<button
className={`fmtm-sticky fmtm-right-2 fmtm-my-auto fmtm-z-50 fmtm-w-fit fmtm-p-1 fmtm-rounded-full fmtm-bg-black fmtm-bg-opacity-50 fmtm-h-fit hover:fmtm-scale-110 fmtm-cursor-pointer fmtm-duration-300`}
onClick={() => {
scrollContainerRef?.current?.scrollBy({ left: 300, behavior: 'smooth' });
}}
>
<AssetModules.ChevronRightIcon className="fmtm-text-white" />
</button>
)}
</div>
</>
);
};

export default ImageSlider;
4 changes: 2 additions & 2 deletions src/frontend/src/components/common/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ const DialogContent = ({
{...props}
>
{children}
<DialogPrimitive.Close className="fmtm-absolute fmtm-right-4 fmtm-top-4 fmtm-rounded-sm fmtm-opacity-70 fmtm-ring-offset-black fmtm-transition-opacity hover:fmtm-opacity-100 focus:fmtm-outline-none disabled:fmtm-pointer-events-none data-[state=open]:fmtm-bg-white data-[state=open]:fmtm-text-black fmtm-w-fit">
<X className="fmtm-h-4 fmtm-w-4" />
<DialogPrimitive.Close className="fmtm-absolute fmtm-z-50 fmtm-right-4 fmtm-top-4 fmtm-rounded-sm fmtm-opacity-70 fmtm-ring-offset-black fmtm-transition-opacity hover:fmtm-opacity-100 focus:fmtm-outline-none disabled:fmtm-pointer-events-none data-[state=open]:fmtm-bg-white data-[state=open]:fmtm-text-black fmtm-w-fit">
<X className="fmtm-h-6 fmtm-w-6 fmtm-text-black fmtm-cursor-pointer hover:fmtm-scale-110 fmtm-duration-150 fmtm-bg-white fmtm-rounded-full fmtm-p-1" />
<span className="fmtm-sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/src/shared/AssetModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ import {
QrCode2Outlined as QrCode2OutlinedIcon,
BarChart as BarChartIcon,
CalendarTodayOutlined as CalendarTodayOutlinedIcon,
ChevronRight as ChevronRightIcon,
ChevronLeft as ChevronLeftIcon,
RotateLeft as RotateLeftIcon,
RotateRight as RotateRightIcon,
} from '@mui/icons-material';
import LockPng from '@/assets/images/lock.png';
import RedLockPng from '@/assets/images/red-lock.png';
Expand Down Expand Up @@ -182,4 +186,8 @@ export default {
QrCode2OutlinedIcon,
BarChartIcon,
CalendarTodayOutlinedIcon,
ChevronRightIcon,
ChevronLeftIcon,
RotateLeftIcon,
RotateRightIcon,
};
8 changes: 8 additions & 0 deletions src/frontend/src/store/slices/SubmissionSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const initialState: SubmissionStateTypes = {
updateReviewStateLoading: false,
mappedVsValidatedTask: [],
mappedVsValidatedTaskLoading: false,
submissionPhotos: [],
submissionPhotosLoading: false,
};

const SubmissionSlice = createSlice({
Expand Down Expand Up @@ -106,6 +108,12 @@ const SubmissionSlice = createSlice({
SetMappedVsValidatedTaskLoading(state, action) {
state.mappedVsValidatedTaskLoading = action.payload;
},
SetSubmissionPhotos(state, action) {
state.submissionPhotos = action.payload;
},
SetSubmissionPhotosLoading(state, action) {
state.submissionPhotosLoading = action.payload;
},
},
});

Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/store/types/ISubmissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type SubmissionStateTypes = {
updateReviewStateLoading: boolean;
mappedVsValidatedTask: mappedVsValidatedTaskType[];
mappedVsValidatedTaskLoading: boolean;
submissionPhotos: string[];
submissionPhotosLoading: boolean;
};

type updateReviewStatusModal = {
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/views/ProjectDetailsV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ const ProjectDetailsV2 = () => {
className={`fmtm-flex fmtm-gap-4 fmtm-absolute fmtm-duration-200 fmtm-z-[1000] fmtm-bg-[#F5F5F5] fmtm-p-2 fmtm-rounded-md ${
toggle
? 'fmtm-left-0 fmtm-bottom-0 lg:fmtm-top-0'
: '-fmtm-left-[60rem] fmtm-bottom-0 lg:fmtm-top-0'
: '-fmtm-left-[65rem] fmtm-bottom-0 lg:fmtm-top-0'
}`}
>
<ProjectOptions projectName={state?.projectInfo?.name} />
Expand Down
Loading

0 comments on commit 21686be

Please sign in to comment.