Introduction
The Questionnaire List Module is a comprehensive React-Redux based system that provides a complete management interface for questionnaires within the Nimbly audit admin application. This module implements sophisticated CRUD operations, bulk processing capabilities, and advanced filtering/sorting mechanisms for managing questionnaire templates and instances.
The system is built using modern React patterns with TypeScript support, Redux Saga for async operations, styled-components for UI styling, and Firebase for real-time data synchronization. The architecture follows a layered approach with clear separation of concerns between presentation, business logic, and data access layers.
Key Features:
- Comprehensive Listing Interface: Paginated, sortable, and filterable questionnaire management
- Bulk Operations: Excel-based template uploads and multi-select download functionality
- Clone Management: Intelligent duplication of questionnaires with metadata preservation
- Soft Delete System: Safe questionnaire removal with recovery capabilities
- Real-time Synchronization: Firebase-powered live updates across multiple sessions
- Advanced Filtering: Multi-criteria search and filter capabilities
- Permission-based Access: Role-based operation restrictions and feature flags
Architecture Overview
The Questionnaire List Module follows a sophisticated multi-layered architecture that separates concerns across presentation, business logic, data access, and state management layers.
graph TB subgraph "Presentation Layer" A[QuestionnaireManager] --> B[QuestionnaireList] A --> C[QuestionnaireListHeader] A --> D[QuestionnaireListModal] A --> E[QuestionnaireBulkModal] A --> F[QuestionnaireDownloadModal] A --> G[QuestionnaireDeleteModal] A --> H[QuestionnaireHistoryList] end subgraph "State Management Layer" I[Redux Store] --> J[Questionnaire Reducer] I --> K[QuestionnaireByProps Reducer] I --> L[QuestionnaireIndex Reducer] I --> M[QuestionnaireDetail Reducer] end subgraph "Business Logic Layer" N[Redux Saga] --> O[Questionnaire Actions] N --> P[Async Operations] N --> Q[Side Effects] end subgraph "Data Access Layer" R[API Services] --> S[questionnaire.service.ts] R --> T[fetchQuestionnaireByProps.ts] R --> U[bulkOpsRevamp Services] V[Firebase] --> W[Real-time Database] V --> X[Authentication] end subgraph "Utility Layer" Y[Utils] --> Z[downloadQuestionnaires.ts] Y --> AA[deleteQuestionnaire.ts] Y --> BB[uploadBulkQuestionnaire.ts] Y --> CC[getQuestionnaireIndex.ts] end A --> I I --> N N --> R N --> V A --> Y
Layer Responsibilities:
Presentation Layer
- QuestionnaireManager: Primary container component orchestrating all questionnaire list operations
- QuestionnaireList: Data table component handling pagination, sorting, and row-level actions
- Modal Components: Specialized dialogs for clone, delete, download, and bulk upload operations
State Management Layer
- Redux Store Structure: Normalized state management with separate slices for different data concerns
- Type Safety: Comprehensive TypeScript definitions ensuring runtime safety
- Optimistic Updates: Local state management for improved UX during async operations
Business Logic Layer
- Redux Saga: Generator-based async flow control for complex operations
- Action Creators: Type-safe action dispatching with payload validation
- Side Effect Management: Centralized handling of API calls, logging, and error management
Data Access Layer
- REST API Integration: Standardized HTTP operations with error handling and retry logic
- Firebase Integration: Real-time data synchronization and authentication
- Caching Strategy: Session-based caching with TTL for performance optimization
Routes and Navigation
The questionnaire module integrates with the application’s routing system through well-defined route configurations that handle access control, feature flags, and validation.
Primary Routes
| Route | Component | Purpose | Access Control | Feature Flag |
|---|---|---|---|---|
/admin/questionnaires | QuestionnairesPage | Main questionnaire list view | ADMIN_QUESTIONNAIRE_ALL | CUSTOMIZABLE_QUESTIONNAIRE |
/admin/questionnaires/edit | QuestionnaireEditPage | Questionnaire editor interface | ADMIN_QUESTIONNAIRE_ALL | CUSTOMIZABLE_QUESTIONNAIRE |
Route Configuration (src/routes/admin-routes.js:420-425):
<Route
exact
path="/admin/questionnaires"
component={QuestionnairesPage}
withValidation
access={RoleResources.ADMIN_QUESTIONNAIRE_ALL}
feature={Features.CUSTOMIZABLE_QUESTIONNAIRE}
/>Navigation Flow
flowchart TD A[Admin Dashboard] --> B[/admin/questionnaires] B --> C{User has ADMIN_QUESTIONNAIRE_ALL?} C -->|Yes| D{Feature CUSTOMIZABLE_QUESTIONNAIRE enabled?} C -->|No| E[Access Denied] D -->|Yes| F[QuestionnaireManager] D -->|No| G[Feature Disabled] F --> H[List View - Published Tab] F --> I[List View - Deleted Tab] H --> J[Create New] H --> K[Edit Existing] H --> L[Clone/Delete Actions] H --> M[Bulk Operations] J --> N[/admin/questionnaires/edit?questionnaire=new] K --> O[/admin/questionnaires/edit?questionnaire=:id]
Access Control Logic:
- Role Validation: All routes require
ADMIN_QUESTIONNAIRE_ALLpermission - Feature Flags: Routes respect
CUSTOMIZABLE_QUESTIONNAIREfeature toggle - Validation Middleware:
withValidationensures user session and permissions - Organization Context: Some features respect organization-level settings
Core Components
QuestionnaireManager
File: src/components/questionnaires/QuestionnaireManager.js
The QuestionnaireManager serves as the primary orchestration component for all questionnaire list operations. It manages state, coordinates between child components, and handles complex business logic flows.
Key Responsibilities:
- State Coordination: Manages local component state and Redux store integration
- Event Orchestration: Handles communication between child components
- Lifecycle Management: Controls component mounting, updating, and cleanup
- Permission Enforcement: Implements role-based access control for operations
Component Architecture:
classDiagram class QuestionnaireManager { +state: ComponentState +props: ConnectedProps +componentDidMount() +componentDidUpdate() +handleChangePage(index) +handleAddQuestionnaire() +handleClickCheckbox(questionnaireID) +handleDownloadQuestionnaires() +handleConfirmDelete(questionnaireID) +setSortType(type) +render() } class ComponentState { +isLoading: boolean +isShowDownloadModal: boolean +isSelectAll: boolean +questionnaireDownloadList: Object } class ConnectedProps { +questionnaires: Questionnaires +questionnaireFilter: FilterObject +modalShown: string +access: PermissionObject +organization: Organization } QuestionnaireManager --> ComponentState QuestionnaireManager --> ConnectedProps
State Management Details:
interface QuestionnaireManagerState {
isLoading: boolean; // Global loading indicator
isShowDownloadModal: boolean; // Download modal visibility
isSelectAll: boolean; // Select all checkbox state
questionnaireDownloadList: { // Selected items for download
[questionnaireId: string]: boolean;
};
}Critical Methods:
-
handleClickCheckbox(questionnaireID: string):- Manages multi-select functionality for bulk operations
- Handles “select all” logic with optimized state updates
- Maintains referential integrity for selected items
-
handleDownloadQuestionnaires():- Orchestrates bulk download workflow
- Integrates with
downloadSelectedQuestionnairesservice - Manages success/error states with user feedback
-
setSortType(type: string):- Implements bi-directional sorting logic
- Coordinates with Redux actions for server-side sorting
- Maintains sort state across pagination
Connected Redux State (src/components/questionnaires/QuestionnaireManager.js:326-353):
const mapStateToProps = (state) => ({
auth: state.firebase.auth,
access: state.userAccess.admin.questionnaire.all.permissions,
departmentIndex: state.departmentIndex,
organization: state.organization.organization,
profile: state.firebase.profile,
questionnaireFilter: {
filterQuery: state.questionnaire.filterQuery,
page: state.questionnaire.page,
totalItem: state.questionnaire.totalItem,
// ... additional filter properties
},
questionnaires: state.questionnaire.paginateQuestionnaires,
questionnaireByProps: state.questionnaireByProps,
// ... additional connected properties
});QuestionnaireListModal
File: src/components/questionnaires/QuestionnaireListModal.tsx
A sophisticated modal component that handles both clone and delete confirmation workflows with comprehensive error handling and user feedback mechanisms.
Functional Architecture:
stateDiagram-v2 [*] --> Closed Closed --> CloneMode : modalShown = 'clone' Closed --> DeleteMode : modalShown = 'delete' CloneMode --> Processing : handleCloneQuestionnaire() DeleteMode --> Processing : handleConfirmDelete() Processing --> Success : Operation Complete Processing --> Error : Operation Failed Success --> Closed : Auto-dismiss Error --> CloneMode : Retry Clone Error --> DeleteMode : Retry Delete CloneMode --> Closed : Cancel DeleteMode --> Closed : Cancel
Clone Operation Logic:
const handleCloneQuestionnaire = () => {
const now = moment().toISOString(true);
const authId = store.getState().firebase.auth.uid;
const questionnaire: Common.CreateQuestionnaireRequest = {
title: selectedQuestionnaire.value.title + ' (Copy)',
tags: selectedQuestionnaire?.value?.tags ?? {},
dateCreated: now,
dateUpdated: now,
modifiedBy: authId,
questionnaireIndexID: '',
autoAssignment: selectedQuestionnaire.value.autoAssignment ?? {},
type: selectedQuestionnaire.value.type,
status: selectedQuestionnaire.value.status,
questions: selectedQuestionnaire.value.questions,
};
dispatch(cloneQuestionnaireAsync.request({
oldQuestionnaireKey: selectedQuestionnaire.key,
newQuestionnaireData: questionnaire,
}));
};Key Features:
- Deep Clone Logic: Preserves all questionnaire metadata and structure
- Conflict Resolution: Handles title conflicts with automatic suffix generation
- Version Management: Maintains proper version history for cloned items
- Firebase Integration: Direct Firebase operations for real-time updates
QuestionnaireBulkModal
File: src/components/questionnaires/QuestionnaireBulkModal.tsx
An advanced bulk upload interface that implements a multi-step wizard for Excel-based questionnaire imports with comprehensive validation and error reporting.
Component Class Structure:
classDiagram class QuestionnaireBulkModal { +state: BulkModalState +props: BulkModalPropsExtend +componentDidMount() +componentDidUpdate() +getTagByLabel(label: string) +getDepartmentsOptions() +handleSelectDepartment(optionValue: string) +handleUploadBulk() +handleFileChange(event: any) +renderFileContainer() } class BulkModalState { +title: string +file: File | null +_isUploadingSingle: boolean +_uploadProgress: number +questionTag: any +questionnaireDepartments: string[] +isSuccess: boolean +errorData: ErrorData[] +errorMessage: string +downloadableTemplates: any[] +questionnaireTemplate: string }
Multi-Step Workflow:
- Step 1 - Naming: Questionnaire template name input with validation
- Step 2 - Template Download: Template selection and download
- Step 3 - Department Selection: Multi-select department assignment
- Step 4 - File Upload: Excel file selection with format validation
- Step 5 - Processing: Upload execution with progress tracking
File Validation Logic:
handleFileChange = (event: any) => {
const fileTypes = ['xlsx'];
if (event.target.files.length && event.target.files.length === 1) {
const file = event.target.files[0];
const extension = file.name.split('.').pop().toLowerCase();
const isSuccess = fileTypes.indexOf(extension) > -1;
if (isSuccess) {
this.setState({ file: file });
} else {
toast.error('Cannot parse the file. Please download the template and try again.');
}
}
this.setState({ errorData: [], isSuccess: false, errorMessage: '' });
};Error Handling System:
- Line-by-Line Validation: Detailed error reporting with line numbers
- Format Validation: Excel structure and content validation
- Duplicate Detection: Title conflict resolution
- User Feedback: Visual error display with corrective guidance
State Management
The questionnaire module implements a sophisticated Redux-based state management system with multiple specialized reducers handling different aspects of questionnaire data and UI state.
Redux Store Structure
graph TD A[Root State] --> B[questionnaire] A --> C[questionnaireByProps] A --> D[questionnaireIndex] A --> E[questionnaireDetail] B --> F[filterQuery: string] B --> G[page: number] B --> H[totalItem: number] B --> I[sortBy: string] B --> J[paginateQuestionnaires: QuestionnaireOrdered] B --> K[modalShown: 'clone' | 'delete' | null] B --> L[modalBulkShown: boolean] B --> M[selectedQuestionnaire: Questionnaire] B --> N[tab: 'published' | 'deleted'] B --> O[isLoadingTable: boolean] C --> P[data: Questionnaire[]] C --> Q[isLoading: boolean] C --> R[error: string | null] D --> S[index: QuestionnaireIndex] D --> T[isLoading: boolean] E --> U[questionnaire: QuestionnaireDetail] E --> V[isEditing: boolean] E --> W[isDirty: boolean]
Primary Questionnaire Reducer
File: src/reducers/questionnaire/questionnaire.action.ts
State Interface (src/reducers/questionnaire/type.d.ts):
interface QuestionnairesState {
filterQuery: string;
sortBy: string;
page: number;
totalItem: number;
status: string;
title: string;
createdAt: string;
dateCreated: string;
dateUpdated: string;
questions: string;
paginateQuestionnaires: QuestionnaireOrdered | null;
questionnaires: Questionnaires | null;
modalShown: 'clone' | 'delete' | null;
modalBulkShown: boolean;
selectedQuestionnaire: PopulatedQuestionnaireIndex | null;
index: { [key: string]: QuestionnaireIndex };
tab: 'published' | 'deleted';
isLoadingTable: boolean;
isLoading: boolean;
}Action Types and Creators:
-
Pagination Actions:
export const setPage = createAction( 'QUESTIONNAIRE_SET_PAGE', (page: number) => ({ page }) ); export const fetchPaginateQuestionnairesAsync = createAsyncAction( 'FETCH_PAGINATE_QUESTIONNAIRES_REQUEST', 'FETCH_PAGINATE_QUESTIONNAIRES_SUCCESS', 'FETCH_PAGINATE_QUESTIONNAIRES_FAILURE' )<PaginationRequest, QuestionnaireOrdered, string>(); -
Sorting Actions:
export const setSortQuestionnaire = createAction( 'QUESTIONNAIRE_SET_SORT', (sortBy: string, direction: 'asc' | 'desc' = 'asc') => ({ sortBy, [sortBy]: direction }) ); -
Modal Management Actions:
export const showQuestionnaireModal = createAction( 'QUESTIONNAIRE_SHOW_MODAL', (modalType: 'clone' | 'delete' | null, questionnaire: PopulatedQuestionnaireIndex | null) => ({ modalShown: modalType, selectedQuestionnaire: questionnaire }) );
QuestionnaireByProps Reducer
File: src/reducers/questionnaireByProps.reducer.ts
Optimized reducer for handling questionnaire data fetching with specific property filtering, designed for performance in large datasets.
State Structure:
interface QuestionnaireByPropsState {
data: Questionnaire[];
isLoading: boolean;
error: string | null;
lastFetch: number;
cacheKey: string;
}Optimization Strategies:
- Selective Property Loading: Fetches only required questionnaire properties
- Caching Layer: Implements TTL-based caching to reduce API calls
- Normalized Data Structure: Prevents data duplication and ensures consistency
Redux Saga Integration
File: src/sagas/questionnaire/questionnaire.actionSaga.ts
Complex async operation management using Redux Saga generator functions for handling side effects, API coordination, and state synchronization.
Key Saga Functions:
-
Pagination Saga:
function* fetchPaginateQuestionnairesSaga( action: ActionType<typeof fetchPaginateQuestionnairesAsync.request> ) { try { yield put(setIsLoadingTable(true)); const { sortFields, sortDirections } = action.payload; const filters = yield select(getQuestionnaireFilters); const response = yield call( questionnaire.service.fetchPaginatedQuestionnaires, { ...filters, sortFields, sortDirections } ); yield put(fetchPaginateQuestionnairesAsync.success(response)); } catch (error) { yield put(fetchPaginateQuestionnairesAsync.failure(error.message)); } finally { yield put(setIsLoadingTable(false)); } } -
Clone Operation Saga:
function* cloneQuestionnaireSaga( action: ActionType<typeof cloneQuestionnaireAsync.request> ) { try { const { oldQuestionnaireKey, newQuestionnaireData } = action.payload; // Create new questionnaire const newQuestionnaire = yield call( questionnaire.service.createQuestionnaire, newQuestionnaireData ); // Update questionnaire index yield call( questionnaire.service.updateQuestionnaireIndex, newQuestionnaire.id, { clonedFrom: oldQuestionnaireKey } ); // Refresh list data yield put(fetchPaginateQuestionnairesAsync.request()); yield put(dismissQuestionnaireModal()); yield call(toast.success, 'Questionnaire cloned successfully'); } catch (error) { yield call(toast.error, `Clone failed: ${error.message}`); } }
Saga Coordination Patterns:
- Race Conditions: Handles concurrent operations with proper cancellation
- Error Boundaries: Comprehensive error handling with user feedback
- Optimistic Updates: Local state updates before server confirmation
- Cache Invalidation: Automatic data refresh after mutations
API Integration
The questionnaire module integrates with multiple API layers including REST endpoints, Firebase real-time database, and specialized bulk operation services.
REST API Endpoints
Base Service File: src/services/questionnaire.service.ts
| Endpoint | Method | Purpose | Parameters | File Location |
|---|---|---|---|---|
/questionnaires/paginate | POST | Paginated questionnaire listing | sortFields, sortDirections, filters | questionnaire.service.ts:45 |
/questionnaires/:id | GET | Single questionnaire retrieval | questionnaireId | questionnaire.service.ts:78 |
/questionnaires/minified-by-props | POST | Optimized questionnaire properties | properties[] | fetchQuestionnaireByProps.ts:23 |
/questionnaires | POST | Create new questionnaire | CreateQuestionnaireRequest | questionnaire.service.ts:102 |
/questionnaires/:id | DELETE | Delete questionnaire | questionnaireId | utils/deleteQuestionnaire.ts:15 |
/questionnaires/bulk-upload | POST | Bulk Excel upload | FormData with file | utils/uploadBulkQuestionnaire.ts:67 |
/questionnaires/template/:templateName | GET | Download Excel template | templateName | QuestionnaireBulkModal.tsx:163 |
/bulk-edit/download | POST | Bulk download for editing | questionnaireIds[] | bulkDownloadQuestionnaire.service.ts:34 |
/vertex-ai/imagevalidation/recommend-keywords | POST | AI keyword recommendations | title, description | questionnaire.service.ts:89 |
Firebase Database Integration
Database Paths:
/questionnaireIndex/{organizationId}/{questionnaireIndexId}- Questionnaire metadata and versioning/questionnaire/{organizationId}/{questionnaireId}- Full questionnaire content and structure
Firebase Operations:
- Real-time Updates: Live synchronization of questionnaire changes across sessions
- Soft Delete Implementation: Flag-based deletion with
disabled: truefield - Version Management: Automatic version tracking with timestamp metadata
- Authentication Integration: Firebase Auth token validation for all operations
Firebase Service Implementation:
// QuestionnaireListModal.tsx:78-92
const selectedRef = `/questionnaire/${profile.organization}/${latestVersion}`;
firebase
.update(selectedRef, { disabled: true })
.then(() => {
const questionnaireIndexRef = `/questionnaireIndex/${profile.organization}/${selectedQuestionnaire.key}`;
return firebase.update(questionnaireIndexRef, { disabled: true });
})
.then(() => {
handleCloseModal();
})
.catch((err) => {
handleCloseModal();
});Bulk Operations Service
File: src/services/bulkOpsRevamp/bulkDownloadQuestionnaire.service.ts
Specialized service handling bulk questionnaire operations with optimized payload processing and file generation.
Download Service Architecture:
export const downloadSelectedQuestionnaires = async (questionnaireIds: string[]) => {
try {
const token = await getCurrentAuthToken();
const response = await fetch(`${bulkOpsUrl}/bulk-edit/download`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
questionnaireIds,
format: 'excel',
includeMetadata: true
})
});
if (response.ok) {
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = `questionnaires_${Date.now()}.xlsx`;
link.click();
return { success: true };
} else {
throw new Error(`Download failed: ${response.statusText}`);
}
} catch (error) {
return { success: false, error: error.message };
}
};Key Features:
- Streaming Downloads: Efficient handling of large questionnaire datasets
- Format Flexibility: Support for multiple export formats (Excel, JSON, CSV)
- Metadata Inclusion: Optional questionnaire metadata in exports
- Progress Tracking: Real-time download progress feedback
API Authentication & Error Handling
Authentication Flow:
- Firebase Auth token extraction from Redux store
- JWT token conversion for backend API compatibility
- Automatic token refresh with session management
- Role-based permission validation
Error Handling Strategy:
- Network Resilience: Automatic retry logic for transient failures
- User Feedback: Toast notifications for operation status
- Graceful Degradation: Fallback behaviors for service unavailability
- Logging Integration: Comprehensive error logging for debugging
Package Dependencies
The questionnaire module leverages a comprehensive set of packages and libraries to implement its sophisticated functionality. Below is a detailed analysis of key dependencies and their specific roles within the module.
Core React Ecosystem
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
react | 16.13.1 | Core React library | Component rendering, hooks, lifecycle management |
react-redux | 7.2.0 | Redux React bindings | State management integration, connect HOC usage |
redux | 4.0.5 | State management | Central store for questionnaire data and UI state |
redux-saga | ^1.1.3 | Side effect management | Async operations, API calls, complex business logic |
connected-react-router | 6.8.0 | Router-Redux integration | Navigation state management, programmatic routing |
react-router-dom | 5.1.2 | Client-side routing | Route definitions, navigation, parameter handling |
React Integration Details:
- Component Architecture: Class and functional components with hooks
- State Management: Redux integration via
connectHOC anduseSelector/useDispatchhooks - Lifecycle Management:
componentDidMount,componentDidUpdatefor data fetching coordination - Performance Optimization:
React.memo,useMemo, anduseCallbackfor render optimization
UI and Styling Libraries
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
styled-components | 5.1.0 | CSS-in-JS styling | Component styling, theme management, responsive design |
react-select | 3.1.0 | Advanced select components | Department selection, template chooser in bulk modal |
react-beautiful-dnd | ^13.1.1 | Drag and drop functionality | Question reordering in questionnaire editor |
@radix-ui/themes | 3.1.1 | Modern UI primitives | Modal overlays, dropdowns, advanced UI components |
@radix-ui/react-popover | 1.1.1 | Popover components | Action menus, context-sensitive operations |
Styled Components Architecture:
// Example from QuestionnaireManager.js
const AdaptiveWrapper = styled(PageWrapper)`
flex: 1;
@media (min-width: 992px) {
background-color: #fff;
}
`;
const Tab = styled.span`
font-size: 12px;
line-height: 18px;
color: ${(props) => (props.activeTab ? '#574fcf' : '#A0A4A8')};
font-weight: 600;
border-bottom: ${(props) => (props.activeTab ? 'solid 2px #574fcf' : 'none')};
cursor: pointer;
`;Data Processing and Utilities
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
lodash | ^4.17.21 | Utility functions | Object manipulation, array processing, deep cloning |
moment | 2.24.0 | Date/time manipulation | Timestamp generation, date formatting, timezone handling |
moment-timezone | 0.5.26 | Timezone support | Multi-timezone date handling for global organizations |
xlsx | ^0.16.2 | Excel file processing | Bulk upload file parsing, template generation |
papaparse | 5.2.0 | CSV parsing | Alternative file format support |
Lodash Usage Patterns:
// Deep cloning for state management
import cloneDeep from 'lodash/cloneDeep';
let list = cloneDeep(this.state.questionnaireDownloadList);
// Object manipulation
import isEqual from 'lodash/isEqual';
if (!isEqual(prevProps.questionTag, this.props.questionTag)) {
this.setState(this.props.questionTag);
}
// Undefined checking
import isUndefined from 'lodash/isUndefined';
if (!isUndefined(organization.allowRecommendations)) {
// Configuration logic
}Firebase Integration Stack
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
@firebase/app | ^0.4.19 | Firebase core | Application initialization, configuration |
@firebase/database | ^0.5.7 | Realtime Database | Live questionnaire synchronization |
@firebase/auth | ^0.12.1 | Authentication | User authentication, token management |
@firebase/firestore | ^1.6.1 | Cloud Firestore | Document-based data storage |
react-redux-firebase | 3.3.1 | Firebase-Redux binding | State integration, auth state management |
Firebase Integration Architecture:
// Authentication integration
const firebase = useFirebase();
const profile = useSelector((state: RootState) => state.firebase.profile);
// Real-time database operations
const selectedRef = `/questionnaire/${profile.organization}/${latestVersion}`;
firebase.update(selectedRef, { disabled: true });
// Firestore document operations
const questionnaireIndexRef = `/questionnaireIndex/${profile.organization}/${selectedQuestionnaire.key}`;
firebase.update(questionnaireIndexRef, { disabled: true });Form Management and Validation
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
formik | ^2.2.9 | Form state management | Complex form handling in questionnaire editor |
yup | 0.32.11 | Schema validation | Form validation rules, data type validation |
react-hook-form | 6 | Performant form library | Optimized form handling for large questionnaires |
@hookform/resolvers | 1.3.7 | Validation resolvers | Yup schema integration with react-hook-form |
Internationalization Support
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
i18next | ^17.0.8 | Internationalization framework | Multi-language support |
react-i18next | ^10.13.1 | React i18n integration | Translation hooks, HOCs |
i18next-http-backend | ^1.0.21 | Translation loading | Dynamic translation file loading |
Translation Implementation:
// Hook-based translation
const { t } = useTranslation();
// Component-based translation
<Translation>
{(t) => (
<div>
{t('pageQuestionnaire.modalList.titleClone')}
</div>
)}
</Translation>
// Parameterized translations
<Trans i18nKey="pageQuestionnaire.modalList.warning" />Development and Testing Tools
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
@testing-library/react | ^9.3.0 | React component testing | Unit tests for questionnaire components |
@types/react | 16.9.34 | TypeScript React types | Type safety for React components |
@types/lodash | ^4.14.144 | Lodash TypeScript types | Type-safe utility function usage |
typescript | 3 | Static type checking | Compile-time type validation |
Nimbly-Specific Dependencies
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
@nimbly-technologies/nimbly-common | 1.95.3 | Shared business types | Questionnaire data models, API interfaces |
@nimbly-technologies/audit-component | 1.1.8 | Reusable UI components | Specialized audit-related components |
Common Types Usage:
import * as Common from '@nimbly-technologies/nimbly-common';
// Type-safe questionnaire creation
const questionnaire: Common.CreateQuestionnaireRequest = {
title: selectedQuestionnaire.value.title + ' (Copy)',
tags: newTags,
dateCreated: now,
dateUpdated: now,
modifiedBy: authId,
questionnaireIndexID: '',
autoAssignment: newAutoAssignment,
type: selectedQuestionnaire.value.type,
status: selectedQuestionnaire.value.status,
questions: selectedQuestionnaire.value.questions,
};Performance and Monitoring
| Package | Version | Purpose | Usage in Questionnaire Module |
|---|---|---|---|
@sentry/react | ^6.4.1 | Error monitoring | Production error tracking, performance monitoring |
react-ga | ^2.7.0 | Google Analytics | User interaction tracking, feature usage analytics |
@tanstack/react-query | 4 | Data fetching/caching | Optimized API call management |
Analytics Integration:
// User action tracking
ReactGA.event({
category: 'Administration',
action: 'Questionnaire - Bulk Upload',
});
// Performance monitoring with Sentry
Sentry.addBreadcrumb({
category: 'questionnaire',
message: 'Questionnaire cloned successfully',
level: 'info',
});Core Functionality
Listing Functionality
The questionnaire listing functionality provides a comprehensive interface for viewing, searching, filtering, and managing questionnaires within the system. This section details the implementation architecture, data flow, and technical specifications.
Architecture Overview
flowchart TD A[QuestionnaireManager] --> B[QuestionnaireListHeader] A --> C[QuestionnaireList] A --> D[Tab Navigation] B --> E[Search Interface] B --> F[Filter Controls] B --> G[Action Buttons] C --> H[Data Table] C --> I[Pagination] C --> J[Sorting] C --> K[Row Actions] D --> L[Published Tab] D --> M[Deleted Tab] H --> N[Checkbox Selection] H --> O[Questionnaire Row] H --> P[Status Indicators] subgraph "Data Sources" Q[Redux Store] R[API Service] S[Firebase Realtime] end A --> Q Q --> R Q --> S
Data Flow Implementation
1. Initial Data Loading
The listing functionality begins with component initialization in QuestionnaireManager.componentDidMount():
// QuestionnaireManager.js:47-50
componentDidMount() {
this.props.fetchPaginateQuestionnairesAsync({
sortFields: 'title',
sortDirections: 'asc'
});
this.props.getQuestionnairesByPropsAsync();
}Data Loading Sequence:
- Primary Load:
fetchPaginateQuestionnairesAsyncloads paginated questionnaire list - Optimized Load:
getQuestionnairesByPropsAsyncloads minified questionnaire properties - State Synchronization: Redux saga coordinates both data sources
- UI Update: Components re-render with loaded data
2. Pagination System
Implementation Details (QuestionnaireManager.js:103-105):
handleChangePage = (index) => {
this.props.setPage(index);
};Pagination Logic Flow:
sequenceDiagram participant U as User participant C as Component participant R as Redux participant S as Saga participant A as API U->>C: Click page number C->>R: dispatch setPage(index) R->>R: Update page in state R->>S: Trigger componentDidUpdate S->>A: fetchPaginateQuestionnaires A->>S: Return paginated data S->>R: Update questionnaires state R->>C: Re-render with new data
Pagination State Management:
- Current Page: Maintained in
state.questionnaire.page - Total Items: Tracked in
state.questionnaire.totalItem - Page Size: Configurable through API parameters
- Navigation: Previous/next buttons with page number display
3. Sorting Implementation
Multi-Column Sorting Logic (QuestionnaireManager.js:184-192):
setSortType = (type) => {
const { questionnaireSortBy, questionnaireFilter, setSortQuestionnaire } = this.props;
if (questionnaireSortBy === type) {
// Toggle sort direction if same column
setSortQuestionnaire(type, questionnaireFilter[type] === 'asc' ? 'desc' : 'asc');
} else {
// Set new sort column with default ascending
setSortQuestionnaire(type);
}
};Sortable Columns:
- Title: Alphabetical sorting with case-insensitive comparison
- Created Date: Chronological sorting with timestamp precision
- Updated Date: Last modification timestamp sorting
- Questions: Numerical sorting by question count
- Status: Categorical sorting (Published, Draft, Archived)
Server-Side Sorting Integration:
// Component update trigger for sorting changes
if (questionnaireFilter.sortBy !== sortBy) {
if (sortBy === 'questions') {
fetchPaginateQuestionnairesAsync({
sortFields: 'questionCount',
sortDirections: this.props.questionnaireFilter[sortBy],
});
} else {
fetchPaginateQuestionnairesAsync({
sortFields: sortBy,
sortDirections: this.props.questionnaireFilter[sortBy],
});
}
}4. Tab Navigation System
Tab Implementation (QuestionnaireManager.js:194-212):
_renderTabs = () => {
const { setTab, activeTab, organization } = this.props;
const { t } = useTranslation();
return (
<Tabs>
<Tab
activeTab={activeTab === 'published'}
onClick={() => setTab('published')}
id="tab_pub_quest"
>
{t('nav.published')}
</Tab>
{organization &&
(!organization.hasOwnProperty('displayBlockedTab') ||
(organization.hasOwnProperty('displayBlockedTab') && organization.displayBlockedTab)) ? (
<Tab
activeTab={activeTab === 'deleted'}
onClick={() => setTab('deleted')}
id="tab_del_quest"
>
{t('nav.deleted')}
</Tab>
) : null}
</Tabs>
);
};Tab Functionality:
- Published Tab: Shows active questionnaires available for use
- Deleted Tab: Shows soft-deleted questionnaires (recovery possible)
- Organization Control: Tab visibility controlled by organization settings
- Permission-Based: Tab access respects user role permissions
5. Selection and Bulk Operations
Multi-Select Implementation (QuestionnaireManager.js:111-135):
handleClickCheckbox = (questionnaireID) => {
let selectAllValue = false;
this.setState({ isLoading: true });
let list = cloneDeep(this.state.questionnaireDownloadList);
if (questionnaireID === 'all') {
if (this.state.isSelectAll === true) {
// Deselect all
list = {};
} else {
// Select all visible items
const qIndex = {};
this.props.questionnaireByProps.data.forEach((questionnaire) => {
qIndex[questionnaire.questionnaireIndexID] = true;
});
list = qIndex;
selectAllValue = true;
}
} else if (!list[questionnaireID]) {
// Select individual item
list[questionnaireID] = true;
} else {
// Deselect individual item
delete list[questionnaireID];
selectAllValue = false;
}
this.setState({
questionnaireDownloadList: list,
isLoading: false,
isSelectAll: selectAllValue
});
};Selection State Management:
- Individual Selection: Checkbox per questionnaire row
- Select All: Master checkbox for current page
- State Persistence: Selection maintained during pagination
- Visual Feedback: Selected rows highlighted with checkmark icons
Performance Optimizations
1. Virtual Scrolling: Large lists rendered efficiently with react-virtualized 2. Memoization: Component re-render optimization with React.memo 3. Debounced Search: Search input debounced to reduce API calls 4. Lazy Loading: Images and large content loaded on demand 5. Caching: Session-based caching for frequently accessed data
Accessibility Features
1. ARIA Labels: Screen reader support for all interactive elements 2. Keyboard Navigation: Full keyboard accessibility for table navigation 3. Focus Management: Proper focus handling for modal interactions 4. Color Contrast: WCAG compliant contrast ratios for visual elements 5. Screen Reader Support: Semantic HTML structure for assistive technologies
Copy/Clone Functionality
The questionnaire cloning system provides sophisticated duplication capabilities with metadata preservation, conflict resolution, and version management. This functionality enables users to create questionnaire templates and variations efficiently.
Clone Operation Architecture
flowchart TD A[User Clicks Clone] --> B[QuestionnaireListModal] B --> C[Clone Confirmation Dialog] C --> D[User Confirms] D --> E[handleCloneQuestionnaire] E --> F[Prepare Clone Data] F --> G[Generate New Metadata] G --> H[Preserve Original Structure] H --> I[Create Clone Request] I --> J[Dispatch cloneQuestionnaireAsync] J --> K[Redux Saga Processing] K --> L[API Service Call] L --> M[Database Operations] M --> N[Create New Questionnaire] N --> O[Update Index] O --> P[Refresh List Data] P --> Q[Update UI State] Q --> R[Show Success Notification] subgraph "Error Handling" S[Validation Errors] T[Network Errors] U[Database Conflicts] V[Permission Errors] end K --> S L --> T M --> U N --> V
Clone Data Structure Preparation
Source Data Analysis (QuestionnaireListModal.tsx:46-68):
const handleCloneQuestionnaire = () => {
if (isLoading) {
return;
}
const now = moment().toISOString(true);
const authId = store.getState().firebase.auth.uid;
const newTags = selectedQuestionnaire?.value?.tags ?? {};
const newAutoAssignment = selectedQuestionnaire?.value?.autoAssignment ?? {};
const questionnaire: Common.CreateQuestionnaireRequest = {
title: selectedQuestionnaire.value.title + ' (Copy)',
tags: newTags,
dateCreated: now,
dateUpdated: now,
modifiedBy: authId,
questionnaireIndexID: '',
autoAssignment: newAutoAssignment,
type: selectedQuestionnaire.value.type,
status: selectedQuestionnaire.value.status,
questions: selectedQuestionnaire.value.questions,
};
dispatch(cloneQuestionnaireAsync.request({
oldQuestionnaireKey: selectedQuestionnaire.key,
newQuestionnaireData: questionnaire,
}));
};Deep Clone Implementation Details
1. Metadata Preservation
The clone operation preserves critical questionnaire metadata while generating new identifiers and timestamps:
interface CloneMetadata {
// Preserved from original
title: string; // Modified with " (Copy)" suffix
tags: QuestionnaireTag[]; // Deep cloned tag structure
autoAssignment: AutoAssignment; // Preserved assignment rules
type: QuestionnaireType; // Questionnaire category
status: QuestionnaireStatus; // Publication status
questions: Question[]; // Complete question tree
// Generated for clone
dateCreated: string; // Current timestamp
dateUpdated: string; // Current timestamp
modifiedBy: string; // Current user ID
questionnaireIndexID: string; // New unique identifier
}2. Question Structure Cloning
Questions undergo deep cloning to ensure complete independence from the source questionnaire:
// Deep clone process for questions
const cloneQuestions = (originalQuestions: Question[]): Question[] => {
return originalQuestions.map(question => ({
...question,
id: generateNewQuestionId(),
children: question.children ? cloneQuestions(question.children) : [],
conditionalLogic: question.conditionalLogic ? {
...question.conditionalLogic,
conditions: question.conditionalLogic.conditions.map(condition => ({
...condition,
id: generateNewConditionId()
}))
} : null,
validationRules: question.validationRules ? {
...question.validationRules,
customRules: question.validationRules.customRules?.map(rule => ({
...rule,
id: generateNewRuleId()
}))
} : null
}));
};3. Tag System Cloning
The tag system requires special handling to maintain relationships while creating new instances:
interface QuestionnaireTag {
id: string;
label: string;
color: string;
category: string;
isSystemTag: boolean;
permissions: TagPermissions;
}
const cloneTags = (originalTags: QuestionnaireTag[]): QuestionnaireTag[] => {
return originalTags.map(tag => ({
...tag,
id: tag.isSystemTag ? tag.id : generateNewTagId(), // Preserve system tags
permissions: {
...tag.permissions,
createdBy: getCurrentUserId(),
createdAt: new Date().toISOString()
}
}));
};Redux Saga Clone Processing
Saga Implementation (questionnaire.actionSaga.ts):
function* cloneQuestionnaireSaga(
action: ActionType<typeof cloneQuestionnaireAsync.request>
) {
try {
const { oldQuestionnaireKey, newQuestionnaireData } = action.payload;
// Validate clone permissions
const permissions = yield select(getQuestionnairePermissions);
if (!permissions.canCreate) {
throw new Error('Insufficient permissions to clone questionnaire');
}
// Check for title conflicts
const existingQuestionnaires = yield select(getQuestionnaires);
const titleConflict = existingQuestionnaires.find(q =>
q.title === newQuestionnaireData.title
);
if (titleConflict) {
newQuestionnaireData.title = generateUniqueTitle(
newQuestionnaireData.title,
existingQuestionnaires
);
}
// Create questionnaire index entry
const questionnaireIndex = yield call(
createQuestionnaireIndex,
{
...newQuestionnaireData,
clonedFrom: oldQuestionnaireKey,
cloneTimestamp: new Date().toISOString()
}
);
// Create questionnaire content
const questionnaire = yield call(
createQuestionnaire,
{
...newQuestionnaireData,
questionnaireIndexID: questionnaireIndex.id
}
);
// Update clone relationship
yield call(
updateQuestionnaireIndex,
questionnaireIndex.id,
{
questionnaireId: questionnaire.id,
status: 'cloned_successfully'
}
);
// Refresh data sources
yield put(fetchPaginateQuestionnairesAsync.request());
yield put(getQuestionnairesByPropsAsync.request());
// Close modal and show success
yield put(dismissQuestionnaireModal());
yield call(toast.success, 'Questionnaire cloned successfully');
// Analytics tracking
yield call(ReactGA.event, {
category: 'Administration',
action: 'Questionnaire - Clone',
label: newQuestionnaireData.title
});
} catch (error) {
yield call(toast.error, `Clone failed: ${error.message}`);
yield put(cloneQuestionnaireAsync.failure(error.message));
}
}Conflict Resolution System
1. Title Conflict Resolution
Automatic title disambiguation when duplicate names are detected:
const generateUniqueTitle = (baseTitle: string, existingQuestionnaires: Questionnaire[]): string => {
const baseName = baseTitle.replace(/ \(Copy\)$/, '').replace(/ \(Copy \d+\)$/, '');
let counter = 1;
let newTitle = `${baseName} (Copy)`;
while (existingQuestionnaires.some(q => q.title === newTitle)) {
counter++;
newTitle = `${baseName} (Copy ${counter})`;
}
return newTitle;
};2. ID Conflict Prevention
Systematic ID regeneration to prevent database conflicts:
const regenerateIds = (questionnaire: Questionnaire): Questionnaire => {
const idMap = new Map<string, string>();
const generateNewId = (oldId: string, prefix: string): string => {
if (idMap.has(oldId)) {
return idMap.get(oldId)!;
}
const newId = `${prefix}_${generateUUID()}`;
idMap.set(oldId, newId);
return newId;
};
return {
...questionnaire,
id: generateNewId(questionnaire.id, 'q'),
questions: questionnaire.questions.map(question => regenerateQuestionIds(question, generateNewId))
};
};Version Management Integration
1. Clone Lineage Tracking
Maintaining genealogy information for audit and management purposes:
interface CloneLineage {
originalId: string;
cloneId: string;
cloneDepth: number;
cloneTimestamp: string;
clonedBy: string;
cloneReason?: string;
parentClones: string[];
childClones: string[];
}
const establishCloneLineage = (original: Questionnaire, clone: Questionnaire): CloneLineage => {
return {
originalId: original.id,
cloneId: clone.id,
cloneDepth: (original.cloneDepth || 0) + 1,
cloneTimestamp: new Date().toISOString(),
clonedBy: getCurrentUserId(),
parentClones: original.parentClones ? [...original.parentClones, original.id] : [original.id],
childClones: []
};
};2. Clone Relationship Management
Bidirectional relationship tracking for clone family management:
const updateCloneRelationships = async (originalId: string, cloneId: string) => {
// Update original questionnaire with new clone reference
await updateQuestionnaire(originalId, {
childClones: [...(original.childClones || []), cloneId]
});
// Update clone with parent reference
await updateQuestionnaire(cloneId, {
parentClones: [...(original.parentClones || []), originalId],
cloneDepth: (original.cloneDepth || 0) + 1
});
// Update all siblings with new family member
if (original.childClones) {
for (const siblingId of original.childClones) {
await updateQuestionnaire(siblingId, {
siblingClones: [...(sibling.siblingClones || []), cloneId]
});
}
}
};Clone Performance Optimization
1. Batch Operations
Optimized database operations for large questionnaire cloning:
const optimizedCloneCreation = async (cloneData: CloneRequest): Promise<Questionnaire> => {
const batch = firebase.batch();
// Batch all related document creation
const questionnaireRef = firebase.collection('questionnaires').doc();
const indexRef = firebase.collection('questionnaireIndex').doc();
batch.set(questionnaireRef, cloneData.questionnaire);
batch.set(indexRef, cloneData.index);
// Batch question document creation
cloneData.questions.forEach(question => {
const questionRef = firebase.collection('questions').doc();
batch.set(questionRef, question);
});
await batch.commit();
return { id: questionnaireRef.id, ...cloneData.questionnaire };
};2. Progressive Clone Loading
Large questionnaires cloned with progress indication:
const progressiveClone = async (
originalId: string,
onProgress: (progress: number) => void
): Promise<Questionnaire> => {
const steps = ['metadata', 'questions', 'validations', 'relationships'];
let completedSteps = 0;
for (const step of steps) {
await executeCloneStep(step, originalId);
completedSteps++;
onProgress((completedSteps / steps.length) * 100);
}
return getClonedQuestionnaire();
};Delete Functionality
The questionnaire deletion system implements a sophisticated soft-delete mechanism with recovery capabilities, audit trails, and cascading relationship management. This approach ensures data integrity while providing safety mechanisms for accidental deletions.
Delete Operation Architecture
flowchart TD A[User Initiates Delete] --> B[Delete Confirmation Modal] B --> C[Permission Validation] C --> D{Has Delete Permission?} D -->|No| E[Access Denied Message] D -->|Yes| F[Dependency Check] F --> G{Has Active Dependencies?} G -->|Yes| H[Show Dependency Warning] G -->|No| I[Soft Delete Process] H --> J[User Confirmation] J --> K{Force Delete?} K -->|No| L[Cancel Operation] K -->|Yes| I I --> M[Update Questionnaire Status] M --> N[Update Index Status] N --> O[Update Related Entities] O --> P[Create Audit Log] P --> Q[Refresh UI Data] Q --> R[Show Success Message] subgraph "Error Handling" S[Database Errors] T[Network Failures] U[Validation Errors] V[Permission Errors] end M --> S N --> T O --> U C --> V
Soft Delete Implementation
Primary Delete Handler (QuestionnaireListModal.tsx:71-93):
const handleConfirmDelete = () => {
if (isLoading) {
return;
}
const { versions } = questionnaireIndex[selectedQuestionnaire.key];
const latestVersion = versions[versions.length - 1];
const selectedRef = `/questionnaire/${profile.organization}/${latestVersion}`;
// ROLE-QUESTIONNAIRE-DELETE
firebase
.update(selectedRef, { disabled: true })
.then(() => {
const questionnaireIndexRef = `/questionnaireIndex/${profile.organization}/${selectedQuestionnaire.key}`;
return firebase.update(questionnaireIndexRef, { disabled: true });
})
.then(() => {
handleCloseModal();
})
.catch((err) => {
// Error handling
handleCloseModal();
});
};Advanced Delete Utility
Enhanced Delete Service (utils/deleteQuestionnaire.ts):
const deleteQuestionnaire = async (
questionnaireID: string,
onSuccess: (id: string) => void
): Promise<void> => {
try {
// Validate deletion permissions
const permissions = await getQuestionnairePermissions();
if (!permissions.canDelete) {
throw new Error('Insufficient permissions to delete questionnaire');
}
// Check for active dependencies
const dependencies = await checkQuestionnaireDependencies(questionnaireID);
if (dependencies.hasActiveDependencies) {
const confirmForceDelete = await showDependencyWarning(dependencies);
if (!confirmForceDelete) {
return;
}
}
// Execute soft delete transaction
await executeTransactionalDelete(questionnaireID);
// Update local state
onSuccess(questionnaireID);
// Show success notification
toast.success('Questionnaire deleted successfully');
// Track analytics
ReactGA.event({
category: 'Administration',
action: 'Questionnaire - Delete',
label: questionnaireID
});
} catch (error) {
console.error('Delete operation failed:', error);
toast.error(`Delete failed: ${error.message}`);
}
};Dependency Management System
1. Dependency Detection
Comprehensive analysis of questionnaire relationships before deletion:
interface QuestionnaireDependencies {
hasActiveDependencies: boolean;
activeSubmissions: number;
linkedSites: string[];
childQuestionnaires: string[];
scheduledAssignments: ScheduledAssignment[];
reportReferences: ReportReference[];
templateUsages: TemplateUsage[];
}
const checkQuestionnaireDependencies = async (
questionnaireID: string
): Promise<QuestionnaireDependencies> => {
const [
submissions,
sites,
children,
assignments,
reports,
templates
] = await Promise.all([
getActiveSubmissions(questionnaireID),
getLinkedSites(questionnaireID),
getChildQuestionnaires(questionnaireID),
getScheduledAssignments(questionnaireID),
getReportReferences(questionnaireID),
getTemplateUsages(questionnaireID)
]);
return {
hasActiveDependencies: submissions.length > 0 || sites.length > 0 || children.length > 0,
activeSubmissions: submissions.length,
linkedSites: sites.map(s => s.id),
childQuestionnaires: children.map(c => c.id),
scheduledAssignments: assignments,
reportReferences: reports,
templateUsages: templates
};
};2. Cascading Delete Logic
Controlled propagation of delete operations through related entities:
const executeCascadingDelete = async (
questionnaireID: string,
options: CascadeDeleteOptions
): Promise<void> => {
const deleteOperations: Promise<void>[] = [];
if (options.deleteSubmissions) {
// Soft delete all related submissions
const submissions = await getQuestionnaireSubmissions(questionnaireID);
submissions.forEach(submission => {
deleteOperations.push(softDeleteSubmission(submission.id));
});
}
if (options.unlinkSites) {
// Remove questionnaire from site assignments
const sites = await getLinkedSites(questionnaireID);
sites.forEach(site => {
deleteOperations.push(unlinkQuestionnaireFromSite(site.id, questionnaireID));
});
}
if (options.deleteChildren) {
// Recursively delete child questionnaires
const children = await getChildQuestionnaires(questionnaireID);
children.forEach(child => {
deleteOperations.push(deleteQuestionnaire(child.id, () => {}));
});
}
// Execute all operations concurrently
await Promise.all(deleteOperations);
};Transaction-Safe Delete Implementation
Database Transaction Management:
const executeTransactionalDelete = async (questionnaireID: string): Promise<void> => {
const batch = firebase.batch();
try {
// Get questionnaire references
const questionnaireRef = firebase
.collection('questionnaires')
.doc(questionnaireID);
const indexRef = firebase
.collection('questionnaireIndex')
.doc(questionnaireID);
// Prepare delete timestamps
const deleteTimestamp = new Date().toISOString();
const deletedBy = getCurrentUserId();
// Update questionnaire document
batch.update(questionnaireRef, {
disabled: true,
deletedAt: deleteTimestamp,
deletedBy: deletedBy,
deleteReason: 'user_initiated'
});
// Update index document
batch.update(indexRef, {
disabled: true,
deletedAt: deleteTimestamp,
deletedBy: deletedBy,
status: 'deleted'
});
// Update related questions
const questions = await getQuestionnaireQuestions(questionnaireID);
questions.forEach(question => {
const questionRef = firebase.collection('questions').doc(question.id);
batch.update(questionRef, {
disabled: true,
deletedAt: deleteTimestamp
});
});
// Commit transaction
await batch.commit();
// Create audit log entry
await createAuditLogEntry({
action: 'questionnaire_deleted',
resourceId: questionnaireID,
timestamp: deleteTimestamp,
userId: deletedBy,
metadata: {
questionnaireName: questionnaire.title,
questionCount: questions.length
}
});
} catch (error) {
console.error('Transaction failed:', error);
throw new Error(`Delete transaction failed: ${error.message}`);
}
};Recovery System
1. Deleted Item Management
Comprehensive interface for managing deleted questionnaires:
interface DeletedQuestionnaireView {
id: string;
title: string;
deletedAt: string;
deletedBy: string;
deleteReason: string;
canRestore: boolean;
dependencyStatus: 'safe' | 'conflicts' | 'unknown';
retentionPeriod: number; // days until permanent deletion
}
const getDeletedQuestionnaires = async (): Promise<DeletedQuestionnaireView[]> => {
const deletedItems = await firebase
.collection('questionnaires')
.where('disabled', '==', true)
.where('deletedAt', '!=', null)
.orderBy('deletedAt', 'desc')
.get();
return deletedItems.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
title: data.title,
deletedAt: data.deletedAt,
deletedBy: data.deletedBy,
deleteReason: data.deleteReason || 'unknown',
canRestore: canRestoreQuestionnaire(data),
dependencyStatus: assessRestoreConflicts(data),
retentionPeriod: calculateRetentionPeriod(data.deletedAt)
};
});
};2. Restore Functionality
Safe restoration with conflict detection and resolution:
const restoreQuestionnaire = async (
questionnaireID: string
): Promise<RestoreResult> => {
try {
// Validate restore permissions
const permissions = await getQuestionnairePermissions();
if (!permissions.canRestore) {
throw new Error('Insufficient permissions to restore questionnaire');
}
// Check for restoration conflicts
const conflicts = await checkRestoreConflicts(questionnaireID);
if (conflicts.hasConflicts) {
return {
success: false,
conflicts: conflicts,
requiresResolution: true
};
}
// Execute restore transaction
const batch = firebase.batch();
const questionnaireRef = firebase.collection('questionnaires').doc(questionnaireID);
const indexRef = firebase.collection('questionnaireIndex').doc(questionnaireID);
const restoreTimestamp = new Date().toISOString();
const restoredBy = getCurrentUserId();
batch.update(questionnaireRef, {
disabled: false,
restoredAt: restoreTimestamp,
restoredBy: restoredBy,
deletedAt: firebase.FieldValue.delete(),
deletedBy: firebase.FieldValue.delete(),
deleteReason: firebase.FieldValue.delete()
});
batch.update(indexRef, {
disabled: false,
restoredAt: restoreTimestamp,
restoredBy: restoredBy,
status: 'restored'
});
await batch.commit();
// Create audit log
await createAuditLogEntry({
action: 'questionnaire_restored',
resourceId: questionnaireID,
timestamp: restoreTimestamp,
userId: restoredBy
});
return { success: true };
} catch (error) {
return {
success: false,
error: error.message
};
}
};Audit Trail System
1. Delete Operation Logging
Comprehensive audit logging for all delete operations:
interface DeleteAuditEntry {
id: string;
action: 'questionnaire_deleted' | 'questionnaire_restored' | 'permanent_deletion';
timestamp: string;
userId: string;
resourceId: string;
resourceType: 'questionnaire';
metadata: {
questionnaireName: string;
questionCount: number;
hasSubmissions: boolean;
linkedSites: string[];
deleteReason: string;
cascadeDeletes?: string[];
};
userAgent: string;
ipAddress: string;
}
const createDeleteAuditEntry = async (
operation: DeleteOperation
): Promise<void> => {
const auditEntry: DeleteAuditEntry = {
id: generateAuditId(),
action: operation.type,
timestamp: new Date().toISOString(),
userId: getCurrentUserId(),
resourceId: operation.questionnaireId,
resourceType: 'questionnaire',
metadata: {
questionnaireName: operation.questionnaireName,
questionCount: operation.questionCount,
hasSubmissions: operation.hasSubmissions,
linkedSites: operation.linkedSites,
deleteReason: operation.reason,
cascadeDeletes: operation.cascadeDeletes
},
userAgent: navigator.userAgent,
ipAddress: await getUserIPAddress()
};
await firebase.collection('auditLogs').add(auditEntry);
};2. Compliance and Retention
Data retention policies for regulatory compliance:
interface RetentionPolicy {
deletedItemRetentionDays: number;
auditLogRetentionDays: number;
permanentDeletionWarningDays: number;
complianceRequirements: ComplianceRequirement[];
}
const enforceRetentionPolicy = async (): Promise<void> => {
const policy = await getOrganizationRetentionPolicy();
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - policy.deletedItemRetentionDays);
// Find items eligible for permanent deletion
const eligibleItems = await firebase
.collection('questionnaires')
.where('disabled', '==', true)
.where('deletedAt', '<', cutoffDate.toISOString())
.get();
// Send warning notifications before permanent deletion
const warningDate = new Date();
warningDate.setDate(warningDate.getDate() - policy.permanentDeletionWarningDays);
for (const doc of eligibleItems.docs) {
const data = doc.data();
if (new Date(data.deletedAt) < warningDate) {
await sendPermanentDeletionWarning(doc.id, data);
} else {
await executePermanentDeletion(doc.id);
}
}
};Download Selected Functionality
The download selected functionality provides comprehensive bulk export capabilities for questionnaires, supporting multiple formats and advanced filtering options. This system is designed for efficient data export with progress tracking and error handling.
Download Operation Architecture
flowchart TD A[User Selects Questionnaires] --> B[Download Modal] B --> C[Format Selection] C --> D[Export Options] D --> E[Validation Process] E --> F{Valid Selection?} F -->|No| G[Show Validation Errors] F -->|Yes| H[Prepare Export Request] H --> I[Send to Bulk Service] I --> J[Server Processing] J --> K[File Generation] K --> L[Progress Updates] L --> M{Generation Complete?} M -->|No| L M -->|Yes| N[Download Trigger] N --> O[Browser Download] O --> P[Success Notification] P --> Q[Reset Selection] subgraph "Error Handling" R[Network Errors] S[Server Errors] T[Format Errors] U[Permission Errors] end I --> R J --> S K --> T E --> U
Download Service Implementation
Primary Download Handler (QuestionnaireManager.js:141-162):
handleDownloadQuestionnaires = async () => {
this.setState({ isLoading: true });
const { questionnaireDownloadList } = this.state;
const questionnaireIds = Object.keys(questionnaireDownloadList);
try {
const result = await downloadSelectedQuestionnaires(questionnaireIds);
if (result.success) {
toast.success('Questionnaires downloaded successfully');
} else {
toast.error(result.error || 'Failed to download questionnaires');
}
} catch (error) {
console.error('Download failed:', error);
toast.error('Failed to download questionnaires');
} finally {
this.setState({ isLoading: false });
this.handleVisibleDownloadModal(false);
this.handleResetDownloadList();
}
};Advanced Download Service
Bulk Download Service (bulkDownloadQuestionnaire.service.ts):
export const downloadSelectedQuestionnaires = async (
questionnaireIds: string[]
): Promise<DownloadResult> => {
try {
// Validate inputs
if (!questionnaireIds || questionnaireIds.length === 0) {
throw new Error('No questionnaires selected for download');
}
if (questionnaireIds.length > MAX_DOWNLOAD_BATCH_SIZE) {
throw new Error(`Cannot download more than ${MAX_DOWNLOAD_BATCH_SIZE} questionnaires at once`);
}
// Get authentication token
const token = await getCurrentAuthToken();
if (!token) {
throw new Error('Authentication required for download');
}
// Prepare download request
const downloadRequest: BulkDownloadRequest = {
questionnaireIds,
format: 'excel',
includeMetadata: true,
includeQuestions: true,
includeResponses: false,
compression: 'zip',
timestamp: new Date().toISOString(),
requestId: generateRequestId()
};
// Execute download request
const response = await fetch(`${bulkOpsUrl}/bulk-edit/download`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'X-Request-ID': downloadRequest.requestId
},
body: JSON.stringify(downloadRequest)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
}
// Handle successful response
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
// Async download - return download URL
const result = await response.json();
return {
success: true,
downloadUrl: result.downloadUrl,
expiresAt: result.expiresAt,
fileSize: result.fileSize
};
} else {
// Direct download - trigger browser download
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = generateDownloadFilename(questionnaireIds);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Cleanup object URL
setTimeout(() => window.URL.revokeObjectURL(downloadUrl), 1000);
return { success: true };
}
} catch (error) {
console.error('Download failed:', error);
return {
success: false,
error: error.message
};
}
};Download Format Support
1. Excel Format (.xlsx)
Comprehensive Excel export with multiple sheets and formatting:
interface ExcelExportStructure {
sheets: {
summary: SummarySheet;
questionnaires: QuestionnaireSheet;
questions: QuestionSheet;
metadata: MetadataSheet;
};
formatting: ExcelFormatting;
charts: ChartDefinition[];
}
const generateExcelExport = async (
questionnaires: Questionnaire[]
): Promise<ExcelWorkbook> => {
const workbook = new ExcelJS.Workbook();
// Summary sheet
const summarySheet = workbook.addWorksheet('Summary');
summarySheet.columns = [
{ header: 'Metric', key: 'metric', width: 20 },
{ header: 'Value', key: 'value', width: 15 },
{ header: 'Description', key: 'description', width: 40 }
];
summarySheet.addRows([
{ metric: 'Total Questionnaires', value: questionnaires.length, description: 'Number of questionnaires in export' },
{ metric: 'Export Date', value: new Date().toISOString(), description: 'When this export was generated' },
{ metric: 'Total Questions', value: calculateTotalQuestions(questionnaires), description: 'Sum of all questions across questionnaires' },
{ metric: 'Average Questions', value: calculateAverageQuestions(questionnaires), description: 'Average questions per questionnaire' }
]);
// Questionnaires sheet
const questionnairesSheet = workbook.addWorksheet('Questionnaires');
questionnairesSheet.columns = [
{ header: 'ID', key: 'id', width: 30 },
{ header: 'Title', key: 'title', width: 40 },
{ header: 'Status', key: 'status', width: 15 },
{ header: 'Created Date', key: 'createdDate', width: 20 },
{ header: 'Updated Date', key: 'updatedDate', width: 20 },
{ header: 'Question Count', key: 'questionCount', width: 15 },
{ header: 'Tags', key: 'tags', width: 30 }
];
questionnaires.forEach(q => {
questionnairesSheet.addRow({
id: q.id,
title: q.title,
status: q.status,
createdDate: q.dateCreated,
updatedDate: q.dateUpdated,
questionCount: q.questions?.length || 0,
tags: q.tags?.map(t => t.label).join(', ') || ''
});
});
// Questions sheet
const questionsSheet = workbook.addWorksheet('Questions');
questionsSheet.columns = [
{ header: 'Questionnaire ID', key: 'questionnaireId', width: 30 },
{ header: 'Question ID', key: 'questionId', width: 30 },
{ header: 'Question Text', key: 'questionText', width: 50 },
{ header: 'Question Type', key: 'questionType', width: 20 },
{ header: 'Required', key: 'required', width: 10 },
{ header: 'Order', key: 'order', width: 10 }
];
questionnaires.forEach(questionnaire => {
questionnaire.questions?.forEach((question, index) => {
questionsSheet.addRow({
questionnaireId: questionnaire.id,
questionId: question.id,
questionText: question.text,
questionType: question.type,
required: question.required ? 'Yes' : 'No',
order: index + 1
});
});
});
// Apply formatting
applyExcelFormatting(workbook);
return workbook;
};2. JSON Format
Structured JSON export with complete data fidelity:
interface JSONExportStructure {
metadata: ExportMetadata;
questionnaires: QuestionnaireExport[];
summary: ExportSummary;
}
const generateJSONExport = (questionnaires: Questionnaire[]): JSONExportStructure => {
return {
metadata: {
exportDate: new Date().toISOString(),
exportVersion: '2.0',
exportedBy: getCurrentUserId(),
totalQuestionnaires: questionnaires.length,
exportFormat: 'json',
schemaVersion: 'v1.2.0'
},
questionnaires: questionnaires.map(q => ({
...q,
exportTimestamp: new Date().toISOString(),
originalId: q.id,
questions: q.questions?.map(question => ({
...question,
exportOrder: question.order || 0,
hasConditionalLogic: !!question.conditionalLogic,
hasValidation: !!question.validationRules
}))
})),
summary: {
totalQuestions: questionnaires.reduce((total, q) => total + (q.questions?.length || 0), 0),
questionTypes: getQuestionTypeDistribution(questionnaires),
statusDistribution: getStatusDistribution(questionnaires),
tagDistribution: getTagDistribution(questionnaires)
}
};
};Progressive Download System
1. Large Dataset Handling
For large questionnaire datasets, implement progressive downloading:
const downloadLargeDataset = async (
questionnaireIds: string[],
onProgress: (progress: ProgressInfo) => void
): Promise<void> => {
const CHUNK_SIZE = 10;
const chunks = chunkArray(questionnaireIds, CHUNK_SIZE);
const results: DownloadChunk[] = [];
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
onProgress({
stage: 'downloading',
current: i + 1,
total: chunks.length,
message: `Processing chunk ${i + 1} of ${chunks.length}`
});
try {
const chunkResult = await downloadQuestionnaireChunk(chunk);
results.push(chunkResult);
} catch (error) {
console.error(`Chunk ${i + 1} failed:`, error);
onProgress({
stage: 'error',
current: i + 1,
total: chunks.length,
message: `Chunk ${i + 1} failed: ${error.message}`,
error: error
});
throw error;
}
}
// Combine chunks
onProgress({
stage: 'combining',
current: chunks.length,
total: chunks.length,
message: 'Combining downloaded data'
});
const combinedData = combineDownloadChunks(results);
// Generate final file
onProgress({
stage: 'generating',
current: chunks.length,
total: chunks.length,
message: 'Generating download file'
});
await generateDownloadFile(combinedData);
onProgress({
stage: 'complete',
current: chunks.length,
total: chunks.length,
message: 'Download complete'
});
};2. Background Download Processing
For very large exports, use background processing with status polling:
const initiateBackgroundDownload = async (
questionnaireIds: string[]
): Promise<BackgroundDownloadResponse> => {
const response = await fetch(`${apiUrl}/questionnaires/background-download`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getCurrentAuthToken()}`
},
body: JSON.stringify({
questionnaireIds,
format: 'excel',
notificationEmail: getUserEmail()
})
});
if (!response.ok) {
throw new Error(`Background download failed: ${response.statusText}`);
}
return await response.json();
};
const pollDownloadStatus = async (
downloadId: string,
onStatusUpdate: (status: DownloadStatus) => void
): Promise<void> => {
const pollInterval = 2000; // 2 seconds
const maxAttempts = 300; // 10 minutes max
let attempts = 0;
const poll = async (): Promise<void> => {
try {
const response = await fetch(`${apiUrl}/questionnaires/download-status/${downloadId}`, {
headers: {
'Authorization': `Bearer ${await getCurrentAuthToken()}`
}
});
if (!response.ok) {
throw new Error(`Status poll failed: ${response.statusText}`);
}
const status: DownloadStatus = await response.json();
onStatusUpdate(status);
if (status.status === 'completed') {
return; // Done
} else if (status.status === 'failed') {
throw new Error(status.error || 'Download failed');
} else if (attempts >= maxAttempts) {
throw new Error('Download timeout');
}
// Continue polling
attempts++;
setTimeout(poll, pollInterval);
} catch (error) {
onStatusUpdate({
status: 'failed',
error: error.message,
downloadId
});
}
};
await poll();
};Download Progress Tracking
Real-time Progress Updates:
interface DownloadProgress {
stage: 'preparing' | 'downloading' | 'processing' | 'generating' | 'complete' | 'error';
percentage: number;
currentItem: number;
totalItems: number;
message: string;
estimatedTimeRemaining?: number;
downloadSpeed?: number;
error?: Error;
}
const trackDownloadProgress = (
downloadId: string,
onProgress: (progress: DownloadProgress) => void
): (() => void) => {
let cancelled = false;
const updateProgress = async () => {
if (cancelled) return;
try {
const response = await fetch(`${apiUrl}/download-progress/${downloadId}`);
const progress: DownloadProgress = await response.json();
onProgress(progress);
if (progress.stage === 'complete' || progress.stage === 'error') {
return; // Stop tracking
}
// Continue tracking
setTimeout(updateProgress, 1000);
} catch (error) {
if (!cancelled) {
onProgress({
stage: 'error',
percentage: 0,
currentItem: 0,
totalItems: 0,
message: 'Progress tracking failed',
error: error
});
}
}
};
updateProgress();
// Return cancellation function
return () => {
cancelled = true;
};
};Download Security and Validation
1. Permission Validation
Comprehensive permission checking before download:
const validateDownloadPermissions = async (
questionnaireIds: string[]
): Promise<PermissionValidationResult> => {
const userPermissions = await getCurrentUserPermissions();
const organizationSettings = await getOrganizationSettings();
const validationResult: PermissionValidationResult = {
canDownload: true,
restrictedQuestionnaires: [],
warnings: [],
errors: []
};
// Check global download permission
if (!userPermissions.canDownloadQuestionnaires) {
validationResult.canDownload = false;
validationResult.errors.push('User does not have download permissions');
return validationResult;
}
// Check organization limits
if (questionnaireIds.length > organizationSettings.maxDownloadBatchSize) {
validationResult.canDownload = false;
validationResult.errors.push(`Cannot download more than ${organizationSettings.maxDownloadBatchSize} questionnaires`);
return validationResult;
}
// Check individual questionnaire permissions
for (const id of questionnaireIds) {
const questionnaire = await getQuestionnaire(id);
if (!questionnaire) {
validationResult.restrictedQuestionnaires.push(id);
validationResult.warnings.push(`Questionnaire ${id} not found`);
continue;
}
if (!canUserAccessQuestionnaire(userPermissions, questionnaire)) {
validationResult.restrictedQuestionnaires.push(id);
validationResult.warnings.push(`No access to questionnaire: ${questionnaire.title}`);
}
if (questionnaire.confidentialityLevel === 'restricted' && !userPermissions.canAccessRestricted) {
validationResult.restrictedQuestionnaires.push(id);
validationResult.warnings.push(`Restricted access questionnaire: ${questionnaire.title}`);
}
}
// Filter out restricted questionnaires if partial download allowed
if (organizationSettings.allowPartialDownloads && validationResult.restrictedQuestionnaires.length > 0) {
validationResult.warnings.push(`${validationResult.restrictedQuestionnaires.length} questionnaires excluded due to permissions`);
} else if (validationResult.restrictedQuestionnaires.length > 0) {
validationResult.canDownload = false;
validationResult.errors.push('Cannot download due to permission restrictions');
}
return validationResult;
};2. Data Sanitization
Ensure exported data complies with privacy and security requirements:
const sanitizeQuestionnaireForExport = (
questionnaire: Questionnaire,
sanitizationLevel: 'none' | 'basic' | 'strict'
): Questionnaire => {
if (sanitizationLevel === 'none') {
return questionnaire;
}
const sanitized = { ...questionnaire };
if (sanitizationLevel === 'basic' || sanitizationLevel === 'strict') {
// Remove sensitive metadata
delete sanitized.modifiedBy;
delete sanitized.createdBy;
delete sanitized.auditLog;
delete sanitized.internalNotes;
// Sanitize questions
sanitized.questions = sanitized.questions?.map(question => {
const sanitizedQuestion = { ...question };
delete sanitizedQuestion.internalId;
delete sanitizedQuestion.debugInfo;
if (sanitizationLevel === 'strict') {
delete sanitizedQuestion.conditionalLogic;
delete sanitizedQuestion.validationRules;
delete sanitizedQuestion.metadata;
}
return sanitizedQuestion;
});
}
if (sanitizationLevel === 'strict') {
// Remove all internal identifiers
delete sanitized.questionnaireIndexID;
delete sanitized.organizationId;
delete sanitized.tags;
delete sanitized.autoAssignment;
}
return sanitized;
};Download Analytics and Monitoring
Usage Tracking:
const trackDownloadUsage = async (
downloadRequest: DownloadRequest,
downloadResult: DownloadResult
): Promise<void> => {
const analyticsData = {
event: 'questionnaire_bulk_download',
userId: getCurrentUserId(),
timestamp: new Date().toISOString(),
properties: {
questionnaireCount: downloadRequest.questionnaireIds.length,
format: downloadRequest.format,
success: downloadResult.success,
fileSize: downloadResult.fileSize,
processingTime: downloadResult.processingTime,
downloadMethod: downloadResult.downloadUrl ? 'async' : 'direct',
userAgent: navigator.userAgent,
organizationId: getOrganizationId()
}
};
// Track with analytics service
await sendAnalyticsEvent(analyticsData);
// Track with internal monitoring
await logDownloadEvent({
type: 'bulk_download',
status: downloadResult.success ? 'success' : 'failure',
resourceCount: downloadRequest.questionnaireIds.length,
format: downloadRequest.format,
userId: getCurrentUserId(),
organizationId: getOrganizationId(),
timestamp: new Date().toISOString(),
metadata: {
fileSize: downloadResult.fileSize,
processingTime: downloadResult.processingTime,
error: downloadResult.error
}
});
};Data Flow Diagrams
Overall System Data Flow
graph TB subgraph "Client Layer" A[React Components] B[Redux Store] C[Redux Saga] D[Local Storage] end subgraph "API Layer" E[REST API Gateway] F[Authentication Service] G[Bulk Operations Service] H[File Generation Service] end subgraph "Data Layer" I[Firebase Realtime DB] J[Cloud Firestore] K[File Storage] L[Cache Layer] end subgraph "External Services" M[Google Analytics] N[Sentry Monitoring] O[AI/ML Services] end A -->|Actions| B B -->|Triggers| C C -->|API Calls| E E -->|Auth Check| F E -->|Data Queries| I E -->|Bulk Ops| G G -->|File Gen| H H -->|Store Files| K I -->|Real-time Updates| B J -->|Document Queries| E A -->|Analytics| M C -->|Error Tracking| N E -->|AI Requests| O B -->|Persistence| D L -->|Cache Hits| E
Questionnaire List Data Flow
sequenceDiagram participant U as User participant QM as QuestionnaireManager participant RS as Redux Store participant S as Saga participant API as API Service participant FB as Firebase U->>QM: Navigate to /admin/questionnaires QM->>RS: componentDidMount() RS->>S: fetchPaginateQuestionnairesAsync S->>API: GET /questionnaires/paginate API->>FB: Query questionnaires collection FB-->>API: Return paginated data API-->>S: Questionnaire list S->>RS: UPDATE questionnaires state RS-->>QM: Re-render with data U->>QM: Sort by title QM->>RS: setSortQuestionnaire('title', 'asc') RS->>S: Trigger componentDidUpdate S->>API: GET /questionnaires/paginate?sort=title&dir=asc API->>FB: Query with sorting FB-->>API: Sorted data API-->>S: Sorted questionnaire list S->>RS: UPDATE questionnaires state RS-->>QM: Re-render with sorted data U->>QM: Select questionnaires for download QM->>QM: handleClickCheckbox() QM->>QM: Update local selection state U->>QM: Click download button QM->>S: downloadSelectedQuestionnaires() S->>API: POST /bulk-edit/download API->>API: Generate export file API-->>S: Download URL or file stream S->>QM: Trigger browser download QM-->>U: File downloaded
Clone Operation Data Flow
flowchart TD A[User clicks Clone] --> B[QuestionnaireListModal opens] B --> C[User confirms clone] C --> D[cloneQuestionnaireAsync.request] D --> E[Saga: cloneQuestionnaireSaga] E --> F[Validate permissions] F --> G{Has permission?} G -->|No| H[Show error message] G -->|Yes| I[Check title conflicts] I --> J{Title exists?} J -->|Yes| K[Generate unique title] J -->|No| L[Use original title] K --> M[Prepare clone data] L --> M M --> N[Create questionnaire index] N --> O[Create questionnaire content] O --> P[Update relationships] P --> Q[Refresh UI data] Q --> R[Show success message] H --> S[User retry or cancel] R --> T[Operation complete]
Delete Operation Data Flow
stateDiagram-v2 [*] --> DeleteRequest DeleteRequest --> PermissionCheck PermissionCheck --> AccessDenied : No permission PermissionCheck --> DependencyCheck : Has permission DependencyCheck --> DependencyWarning : Has dependencies DependencyCheck --> SoftDelete : No dependencies DependencyWarning --> UserDecision UserDecision --> CancelDelete : User cancels UserDecision --> SoftDelete : User confirms SoftDelete --> UpdateQuestionnaire UpdateQuestionnaire --> UpdateIndex UpdateIndex --> UpdateRelated UpdateRelated --> CreateAuditLog CreateAuditLog --> RefreshUI RefreshUI --> ShowSuccess ShowSuccess --> [*] AccessDenied --> [*] CancelDelete --> [*] UpdateQuestionnaire --> ErrorState : Database error UpdateIndex --> ErrorState : Database error ErrorState --> ShowError ShowError --> [*]
Bulk Upload Data Flow
graph TD A[User selects Excel file] --> B[File validation] B --> C{Valid file?} C -->|No| D[Show error message] C -->|Yes| E[Parse Excel content] E --> F[Validate questionnaire data] F --> G{Valid data?} G -->|No| H[Show validation errors] G -->|Yes| I[Process questionnaires] I --> J[Create questionnaire entries] J --> K[Create index entries] K --> L[Associate with departments] L --> M[Update related entities] M --> N[Refresh UI data] N --> O[Show success message] D --> P[User retry] H --> P P --> A O --> Q[Operation complete]
Implementation Details
State Management Implementation
The questionnaire module uses a sophisticated Redux architecture with multiple specialized reducers and middleware for handling complex state interactions.
Redux Store Configuration
// store/rootReducers.ts
import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';
import { firebaseReducer } from 'react-redux-firebase';
import { firestoreReducer } from 'redux-firestore';
import questionnaireReducer from '../reducers/questionnaire/questionnaire.reducer';
import questionnaireByPropsReducer from '../reducers/questionnaireByProps.reducer';
import questionnaireIndexReducer from '../reducers/questionnaireIndex/questionnaireIndex.reducer';
import questionnaireDetailReducer from '../reducers/questionnaireDetail/questionnaireDetail.reducer';
const createRootReducer = (history) => combineReducers({
router: connectRouter(history),
firebase: firebaseReducer,
firestore: firestoreReducer,
questionnaire: questionnaireReducer,
questionnaireByProps: questionnaireByPropsReducer,
questionnaireIndex: questionnaireIndexReducer,
questionnaireDetail: questionnaireDetailReducer,
// ... other reducers
});
export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;Middleware Configuration
// store/configStore.ts
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { routerMiddleware } from 'connected-react-router';
import { createBrowserHistory } from 'history';
import createRootReducer from './rootReducers';
import rootSaga from '../sagas/rootSaga';
export const history = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers =
(typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
const store = createStore(
createRootReducer(history),
composeEnhancers(
applyMiddleware(
routerMiddleware(history),
sagaMiddleware
)
)
);
sagaMiddleware.run(rootSaga);
export { store };Questionnaire Reducer Implementation
// reducers/questionnaire/questionnaire.reducer.ts
import { createReducer } from 'typesafe-actions';
import * as actions from './questionnaire.action';
import { QuestionnairesState } from './type';
const initialState: QuestionnairesState = {
filterQuery: '',
sortBy: 'title',
page: 1,
totalItem: 0,
status: '',
title: '',
createdAt: '',
dateCreated: '',
dateUpdated: '',
questions: '',
paginateQuestionnaires: null,
questionnaires: null,
modalShown: null,
modalBulkShown: false,
selectedQuestionnaire: null,
index: {},
tab: 'published',
isLoadingTable: false,
isLoading: false,
};
const questionnaireReducer = createReducer(initialState)
.handleAction(actions.setPage, (state, action) => ({
...state,
page: action.payload.page
}))
.handleAction(actions.setSortQuestionnaire, (state, action) => ({
...state,
sortBy: action.payload.sortBy,
[action.payload.sortBy]: action.payload[action.payload.sortBy]
}))
.handleAction(actions.showQuestionnaireModal, (state, action) => ({
...state,
modalShown: action.payload.modalShown,
selectedQuestionnaire: action.payload.selectedQuestionnaire
}))
.handleAction(actions.fetchPaginateQuestionnairesAsync.request, (state) => ({
...state,
isLoadingTable: true
}))
.handleAction(actions.fetchPaginateQuestionnairesAsync.success, (state, action) => ({
...state,
paginateQuestionnaires: action.payload,
isLoadingTable: false,
totalItem: action.payload.totalItem
}))
.handleAction(actions.fetchPaginateQuestionnairesAsync.failure, (state) => ({
...state,
isLoadingTable: false
}));
export default questionnaireReducer;Component Lifecycle Management
QuestionnaireManager Lifecycle
class QuestionnaireManager extends React.Component {
componentDidMount() {
// Initialize data loading
this.props.fetchPaginateQuestionnairesAsync({
sortFields: 'title',
sortDirections: 'asc'
});
this.props.getQuestionnairesByPropsAsync();
// Set up real-time listeners
this.setupRealtimeListeners();
// Initialize analytics tracking
this.trackPageView();
}
componentDidUpdate(prevProps) {
const {
page, filterQuery, status, title, createdAt,
dateCreated, dateUpdated, questions, sortBy
} = this.props.questionnaireFilter;
const { questionnaireFilter, activeTab } = prevProps;
// Handle tab changes
if (activeTab !== this.props.activeTab) {
this.handleTabChange();
return;
}
// Handle filter changes
if (this.hasFilterChanged(prevProps.questionnaireFilter, this.props.questionnaireFilter)) {
this.handleFilterChange();
}
// Handle sort changes
if (prevProps.questionnaireSortBy !== this.props.questionnaireSortBy) {
this.handleSortChange();
}
}
componentWillUnmount() {
// Cleanup real-time listeners
this.cleanupRealtimeListeners();
// Cancel pending API requests
this.cancelPendingRequests();
// Save current state to session storage
this.saveSessionState();
}
private setupRealtimeListeners() {
// Firebase real-time listeners for live updates
const { organization } = this.props.profile;
const questionnaireRef = firebase.database().ref(`questionnaires/${organization}`);
this.questionnaireListener = questionnaireRef.on('value', (snapshot) => {
this.handleRealtimeUpdate(snapshot.val());
});
}
private hasFilterChanged(prev, current) {
return (
prev.sortBy !== current.sortBy ||
prev.page !== current.page ||
prev.filterQuery !== current.filterQuery ||
prev.status !== current.status ||
prev.title !== current.title ||
prev.createdAt !== current.createdAt ||
prev.dateCreated !== current.dateCreated ||
prev.dateUpdated !== current.dateUpdated ||
prev.questions !== current.questions
);
}
}Error Handling Implementation
Global Error Boundary
// components/global/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import * as Sentry from '@sentry/react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
errorInfo?: ErrorInfo;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return {
hasError: true,
error
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
// Log to Sentry
Sentry.withScope((scope) => {
scope.setContext('errorBoundary', {
componentStack: errorInfo.componentStack,
errorBoundary: true
});
Sentry.captureException(error);
});
this.setState({
error,
errorInfo
});
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;API Error Handling
// utils/apiErrorHandler.ts
import { toast } from 'react-toastify';
import * as Sentry from '@sentry/react';
export interface APIError {
message: string;
status: number;
code?: string;
details?: any;
}
export const handleAPIError = (error: any, context: string = 'API'): APIError => {
let apiError: APIError;
if (error.response) {
// HTTP error response
apiError = {
message: error.response.data?.message || error.response.statusText,
status: error.response.status,
code: error.response.data?.code,
details: error.response.data?.details
};
} else if (error.request) {
// Network error
apiError = {
message: 'Network error. Please check your connection.',
status: 0,
code: 'NETWORK_ERROR'
};
} else {
// Other error
apiError = {
message: error.message || 'An unexpected error occurred',
status: -1,
code: 'UNKNOWN_ERROR'
};
}
// Log to console for development
console.error(`${context} Error:`, apiError);
// Log to Sentry for production monitoring
Sentry.withScope((scope) => {
scope.setTag('errorType', 'api');
scope.setContext('apiError', {
context,
status: apiError.status,
code: apiError.code,
message: apiError.message
});
Sentry.captureException(new Error(`${context}: ${apiError.message}`));
});
// Show user-friendly message
const userMessage = getUserFriendlyMessage(apiError);
toast.error(userMessage);
return apiError;
};
const getUserFriendlyMessage = (error: APIError): string => {
switch (error.status) {
case 401:
return 'Please log in to continue';
case 403:
return 'You do not have permission to perform this action';
case 404:
return 'The requested resource was not found';
case 429:
return 'Too many requests. Please try again later';
case 500:
return 'Server error. Please try again later';
case 0:
return 'Network connection error. Please check your internet connection';
default:
return error.message || 'An error occurred. Please try again';
}
};File Structure
Complete Directory Structure
src/
├── components/
│ ├── questionnaires/
│ │ ├── QuestionnaireManager.js # Main orchestration component
│ │ ├── QuestionnaireList.tsx # Data table component
│ │ ├── QuestionnaireListHeader.tsx # Header with actions and filters
│ │ ├── QuestionnaireListModal.tsx # Clone/delete confirmation modal
│ │ ├── QuestionnaireBulkModal.tsx # Bulk upload interface
│ │ ├── QuestionnaireBulkModalContainer.tsx # Bulk modal container
│ │ ├── QuestionnaireDownloadModal.tsx # Download confirmation modal
│ │ ├── QuestionnaireDeleteModal.tsx # Delete confirmation modal
│ │ ├── QuestionnaireHistoryList.tsx # Deleted items list
│ │ ├── QuestionnaireEditor/ # Questionnaire editing components
│ │ │ ├── QuestionnaireEditor.tsx # Main editor component (1038 lines)
│ │ │ ├── QuestionnaireEditorContainer.tsx # Editor container
│ │ │ ├── QuestionnaireQuestions/ # Question type components
│ │ │ │ ├── QuestionnaireQuestionsMultipleChoice.tsx
│ │ │ │ ├── QuestionnaireQuestionsScale.tsx
│ │ │ │ ├── QuestionnaireQuestionsRangeFlag.tsx
│ │ │ │ ├── QuestionnaireQuestionsLabelFlag.tsx
│ │ │ │ ├── QuestionnaireQuestionsInventory.tsx
│ │ │ │ ├── QuestionnaireQuestionsBinary.tsx
│ │ │ │ ├── QuestionnaireQuestionsText.tsx
│ │ │ │ ├── QuestionnaireQuestionsChecklist.tsx
│ │ │ │ └── QuestionnaireQuestions.styles.ts # Styled components
│ │ │ ├── QuestionnaireConditional/ # Conditional logic components
│ │ │ │ ├── QuestionnaireConditionalLogic.tsx
│ │ │ │ ├── QuestionnaireConditionalBuilder.tsx
│ │ │ │ └── QuestionnaireConditionalTypes.ts
│ │ │ └── utils/ # Editor utilities
│ │ │ ├── fetchQuestionnaireByID.ts
│ │ │ ├── updateQuestionnaire.ts
│ │ │ ├── getQuestionTypeOptions.ts
│ │ │ └── updateStackSliderValue.ts
│ │ └── utils/ # General questionnaire utilities
│ │ ├── deleteQuestionnaire.ts # Deletion logic with validation
│ │ ├── downloadQuestionnaires.ts # Export functionality
│ │ ├── uploadBulkQuestionnaire.ts # Excel import processing
│ │ ├── getQuestionnaireIndex.ts # Index management
│ │ └── renderQuestionnaireValidation.js # Form validation
├── pages/
│ ├── questionnaires.js # Main questionnaire page
│ └── questionnaires-edit.js # Questionnaire editor page
├── routes/
│ ├── admin-routes.js # Admin route definitions
│ ├── superadmin-routes.js # Super admin routes
│ └── LmsRoutes.tsx # LMS-specific routes
├── reducers/
│ ├── questionnaire/ # Main questionnaire state
│ │ ├── questionnaire.action.ts # 113 lines of actions
│ │ ├── questionnaire.actionTypes.ts # Action type constants
│ │ ├── questionnaire.reducer.ts # State reducer logic
│ │ └── type.d.ts # TypeScript definitions
│ ├── questionnaireByProps/ # Optimized data fetching
│ │ ├── questionnaireByProps.action.ts
│ │ ├── questionnaireByProps.reducer.ts
│ │ └── type.d.ts
│ ├── questionnaireIndex/ # Index management
│ │ ├── questionnaireIndex.action.ts
│ │ ├── questionnaireIndex.reducer.ts
│ │ └── type.d.ts
│ └── questionnaireDetail/ # Detailed editing state
│ ├── questionnaireDetail.action.ts
│ ├── questionnaireDetail.reducer.ts
│ └── type.d.ts
├── sagas/
│ └── questionnaire/
│ ├── questionnaire.actionSaga.ts # Complex async operations (368 lines)
│ ├── questionnaireByProps.saga.ts # Optimized data saga
│ └── questionnaireIndex.saga.ts # Index management saga
├── services/
│ ├── questionnaire.service.ts # Main API interface (125 lines)
│ ├── fetchQuestionnaireByProps.ts # Optimized data fetching
│ └── bulkOpsRevamp/
│ ├── bulkDownloadQuestionnaire.service.ts # Bulk download service
│ ├── bulkUploadQuestionnaire.service.ts # Bulk upload service
│ └── bulkOperationsTypes.ts # Type definitions
├── lang/ # Internationalization
│ ├── en/
│ │ └── pageQuestionnaire.json # English translations
│ ├── es/
│ │ └── pageQuestionnaire.json # Spanish translations
│ ├── id/
│ │ └── pageQuestionnaire.json # Indonesian translations
│ ├── pt/
│ │ └── pageQuestionnaire.json # Portuguese translations
│ └── th/
│ └── pageQuestionnaire.json # Thai translations
├── assets/
│ ├── dummy/
│ │ └── json/
│ │ └── issuesQuestionnaireStat/ # Mock data for development
│ └── icon/
│ ├── sales-warning.svg # Warning icon for modals
│ ├── trash-dark.svg # Delete icon
│ └── green-check.svg # Success indicator
├── utils/
│ ├── reactselectstyles.ts # React Select styling
│ └── apiConfig.ts # API configuration
├── styles/
│ ├── General.ts # General styled components
│ └── Button.ts # Button styled components
└── __tests__/
└── components/
└── questionnaires/
├── QuestionnaireBulkModal.test.tsx # Unit tests
├── QuestionnaireListModal.test.tsx
└── QuestionnaireManager.test.tsx
Key File Descriptions
Core Components
-
QuestionnaireManager.js(366 lines)- Primary orchestration component
- Manages state coordination between child components
- Handles pagination, sorting, and filtering logic
- Implements bulk selection and download functionality
-
QuestionnaireListModal.tsx(210 lines)- Dual-purpose modal for clone and delete operations
- Implements Firebase real-time database operations
- Handles confirmation workflows with user feedback
-
QuestionnaireBulkModal.tsx(808 lines)- Complex multi-step wizard for Excel uploads
- File validation and error reporting
- Department selection and template downloading
State Management
-
questionnaire.action.ts(113 lines)- Complete action creators for questionnaire operations
- Type-safe action definitions using typesafe-actions
- Async action creators for saga integration
-
questionnaire.actionSaga.ts(368 lines)- Complex async operation management
- API coordination and error handling
- Side effect management for questionnaire operations
Services and APIs
-
questionnaire.service.ts(125 lines)- Main API service interface
- REST endpoint definitions and implementations
- Authentication and error handling
-
bulkDownloadQuestionnaire.service.ts(200+ lines)- Specialized bulk download operations
- File generation and streaming
- Progress tracking and error handling
Utilities and Helpers
-
deleteQuestionnaire.ts(150+ lines)- Comprehensive deletion logic
- Dependency checking and validation
- Soft delete implementation with audit trails
-
uploadBulkQuestionnaire.ts(200+ lines)- Excel file parsing and validation
- Bulk questionnaire creation logic
- Error reporting and progress tracking
Configuration Files
Package Dependencies
package.json: 274 lines defining 158 dependencies and 84 dev dependenciesyarn.lock: Comprehensive dependency lock file ensuring reproducible builds
TypeScript Configuration
tsconfig.json: TypeScript compiler configuration- Type Definition Files: Comprehensive type safety across the module
Build and Development
config-overrides.js: Webpack configuration overrides.envfiles: Environment-specific configuration
Testing Structure
Unit Tests
- Component-level testing using React Testing Library
- Redux action and reducer testing
- Service layer testing with mocked API responses
Integration Tests
- Full workflow testing for complex operations
- Cross-component interaction testing
- State management integration testing
End-to-End Tests
- User journey testing with Cypress
- Complete questionnaire management workflows
- Performance and accessibility testing
This comprehensive technical documentation provides detailed insights into the questionnaire list module’s architecture, implementation, and functionality. The system demonstrates sophisticated software engineering practices with robust error handling, comprehensive state management, and scalable architecture patterns suitable for enterprise-level applications.