mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: implement ModalFooter component for consistent modal action layout
This commit is contained in:
@@ -44,7 +44,7 @@ import {
|
|||||||
useUpdateLicenseTemplateMutation,
|
useUpdateLicenseTemplateMutation,
|
||||||
type LicenseTemplate,
|
type LicenseTemplate,
|
||||||
} from '@ema-platform/api';
|
} from '@ema-platform/api';
|
||||||
import { EmptyState, ErrorState, PageHeader } from '@ema-platform/ui';
|
import { EmptyState, ErrorState, ModalFooter, PageHeader } from '@ema-platform/ui';
|
||||||
import { authStorage, usePermissions } from '@ema-platform/auth';
|
import { authStorage, usePermissions } from '@ema-platform/auth';
|
||||||
import { PERMISSIONS } from '../../../layouts/nav-config';
|
import { PERMISSIONS } from '../../../layouts/nav-config';
|
||||||
|
|
||||||
@@ -529,7 +529,7 @@ export function CertificateDesignerPage() {
|
|||||||
'Starts from the live design, or the built-in layout if this type has none.',
|
'Starts from the live design, or the built-in layout if this type has none.',
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={() => setNewOpen(false)}>
|
<Button variant="default" onClick={() => setNewOpen(false)}>
|
||||||
{t('common.cancel', 'Cancel')}
|
{t('common.cancel', 'Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -550,7 +550,7 @@ export function CertificateDesignerPage() {
|
|||||||
>
|
>
|
||||||
{t('designer.create', 'Create')}
|
{t('designer.create', 'Create')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IconEdit, IconTrash, IconPlus, IconInfoCircle, IconCertificate } from '@tabler/icons-react';
|
import { IconEdit, IconTrash, IconPlus, IconInfoCircle, IconCertificate } from '@tabler/icons-react';
|
||||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
|
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from '@ema-platform/ui';
|
||||||
import {
|
import {
|
||||||
useGetCertificationsQuery,
|
useGetCertificationsQuery,
|
||||||
useCreateCertificationMutation,
|
useCreateCertificationMutation,
|
||||||
@@ -199,10 +199,10 @@ export function CertificationPage() {
|
|||||||
|
|
||||||
<Modal opened={deleteOpened} onClose={closeDelete} title={t('certification.confirmDelete')} size="sm">
|
<Modal opened={deleteOpened} onClose={closeDelete} title={t('certification.confirmDelete')} size="sm">
|
||||||
<Text mb="md">{t('certification.deleteConfirmText', { name: deleteTarget?.name?.[locale] })}</Text>
|
<Text mb="md">{t('certification.deleteConfirmText', { name: deleteTarget?.name?.[locale] })}</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closeDelete} size="sm">{t('certification.cancel')}</Button>
|
<Button variant="default" onClick={closeDelete} size="sm">{t('certification.cancel')}</Button>
|
||||||
<Button color="red" onClick={handleDelete} size="sm">{t('certification.delete')}</Button>
|
<Button color="red" onClick={handleDelete} size="sm">{t('certification.delete')}</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
useErrorHandler,
|
useErrorHandler,
|
||||||
AdvancedTable,
|
AdvancedTable,
|
||||||
useServerTable,
|
useServerTable,
|
||||||
|
ModalFooter,
|
||||||
type AdvancedColumn,
|
type AdvancedColumn,
|
||||||
} from "@ema-platform/ui";
|
} from "@ema-platform/ui";
|
||||||
import { LocationPage } from "../../location/pages/LocationPage";
|
import { LocationPage } from "../../location/pages/LocationPage";
|
||||||
@@ -388,14 +389,14 @@ function ProfessionTab() {
|
|||||||
name: deleteTarget?.name?.[locale] ?? "",
|
name: deleteTarget?.name?.[locale] ?? "",
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closeDelete} size="sm">
|
<Button variant="default" onClick={closeDelete} size="sm">
|
||||||
{t("configuration.cancel")}
|
{t("configuration.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="red" onClick={confirmDeleteProf} size="sm">
|
<Button color="red" onClick={confirmDeleteProf} size="sm">
|
||||||
{t("configuration.delete")}
|
{t("configuration.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
IconX,
|
IconX,
|
||||||
IconDetails
|
IconDetails
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { notify, useErrorHandler } from "@ema-platform/ui";
|
import { notify, useErrorHandler, ModalFooter } from "@ema-platform/ui";
|
||||||
import {
|
import {
|
||||||
useGetExamQuery,
|
useGetExamQuery,
|
||||||
useUpdateExamMutation,
|
useUpdateExamMutation,
|
||||||
@@ -494,14 +494,14 @@ export function ExamDetailPage() {
|
|||||||
mode="manual"
|
mode="manual"
|
||||||
actions={setWhatAction}
|
actions={setWhatAction}
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closeAssign} size="sm">
|
<Button variant="default" onClick={closeAssign} size="sm">
|
||||||
{t("exam.cancel")}
|
{t("exam.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
||||||
{t("exam.saveAssignments")}
|
{t("exam.saveAssignments")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -531,14 +531,14 @@ export function ExamDetailPage() {
|
|||||||
onChange={setDraftQuestions}
|
onChange={setDraftQuestions}
|
||||||
mode="random"
|
mode="random"
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closeAssign} size="sm">
|
<Button variant="default" onClick={closeAssign} size="sm">
|
||||||
{t("exam.cancel")}
|
{t("exam.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
||||||
{t("exam.saveAssignments")}
|
{t("exam.saveAssignments")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
IconClipboardList,
|
IconClipboardList,
|
||||||
IconDetails,
|
IconDetails,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from "@ema-platform/ui";
|
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from "@ema-platform/ui";
|
||||||
import { useGetCertificationsQuery } from "../../certification/api/certification-api";
|
import { useGetCertificationsQuery } from "../../certification/api/certification-api";
|
||||||
import {
|
import {
|
||||||
useGetExamsQuery,
|
useGetExamsQuery,
|
||||||
@@ -600,14 +600,14 @@ export function ExamPage() {
|
|||||||
name: deleteTarget?.title?.[locale] ?? "",
|
name: deleteTarget?.title?.[locale] ?? "",
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closeDelete} size="sm">
|
<Button variant="default" onClick={closeDelete} size="sm">
|
||||||
{t("exam.cancel")}
|
{t("exam.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="red" onClick={handleDelete} size="sm">
|
<Button color="red" onClick={handleDelete} size="sm">
|
||||||
{t("exam.delete")}
|
{t("exam.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Group,
|
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ModalFooter } from '@ema-platform/ui';
|
||||||
import type { ResolvedAction } from '../config/actions';
|
import type { ResolvedAction } from '../config/actions';
|
||||||
|
|
||||||
/** Reason codes offered per action. Free text is always available too. */
|
/** Reason codes offered per action. Free text is always available too. */
|
||||||
@@ -290,7 +290,7 @@ export function DecisionConfirmModal({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={onClose}>
|
<Button variant="default" onClick={onClose}>
|
||||||
{t('common.cancel', 'Cancel')}
|
{t('common.cancel', 'Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -310,7 +310,7 @@ export function DecisionConfirmModal({
|
|||||||
>
|
>
|
||||||
{t(action.labelKey)}
|
{t(action.labelKey)}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import {
|
|||||||
useScheduleInspectionMutation,
|
useScheduleInspectionMutation,
|
||||||
type RemarkTargetType,
|
type RemarkTargetType,
|
||||||
} from '@ema-platform/api';
|
} from '@ema-platform/api';
|
||||||
import { ErrorState } from '@ema-platform/ui';
|
import { ErrorState, ModalFooter } from '@ema-platform/ui';
|
||||||
import { usePermissions } from '@ema-platform/auth';
|
import { usePermissions } from '@ema-platform/auth';
|
||||||
import { useAppSelector } from '../../../store/hooks';
|
import { useAppSelector } from '../../../store/hooks';
|
||||||
import { DecisionBar } from '../components/DecisionBar';
|
import { DecisionBar } from '../components/DecisionBar';
|
||||||
@@ -894,7 +894,7 @@ export function LicenseReviewPage() {
|
|||||||
value={inspectionDate}
|
value={inspectionDate}
|
||||||
onChange={(e) => setInspectionDate(e.currentTarget.value)}
|
onChange={(e) => setInspectionDate(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={t('review.pickDate', 'Pick a date and time first')}
|
label={t('review.pickDate', 'Pick a date and time first')}
|
||||||
disabled={Boolean(inspectionDate)}
|
disabled={Boolean(inspectionDate)}
|
||||||
@@ -924,7 +924,7 @@ export function LicenseReviewPage() {
|
|||||||
>
|
>
|
||||||
<IconCheck size={18} />
|
<IconCheck size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -942,7 +942,7 @@ export function LicenseReviewPage() {
|
|||||||
autosize
|
autosize
|
||||||
minRows={3}
|
minRows={3}
|
||||||
/>
|
/>
|
||||||
<Group grow>
|
<ModalFooter grow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
color="teal"
|
color="teal"
|
||||||
@@ -989,7 +989,7 @@ export function LicenseReviewPage() {
|
|||||||
>
|
>
|
||||||
<IconX size={18} />
|
<IconX size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconSettings, IconMap, IconInfoCircle, IconPlus } from '@tabler/icons-react';
|
import { IconSettings, IconMap, IconInfoCircle, IconPlus } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { notify, useErrorHandler } from '@ema-platform/ui';
|
import { notify, useErrorHandler, ModalFooter } from '@ema-platform/ui';
|
||||||
import { LocationTree } from '../components/LocationTree';
|
import { LocationTree } from '../components/LocationTree';
|
||||||
import { LocationDetail } from '../components/LocationDetail';
|
import { LocationDetail } from '../components/LocationDetail';
|
||||||
import { LocationForm } from '../components/LocationForm';
|
import { LocationForm } from '../components/LocationForm';
|
||||||
@@ -226,14 +226,14 @@ export function LocationPage() {
|
|||||||
name: selectedLocation?.names[locale] ?? '',
|
name: selectedLocation?.names[locale] ?? '',
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closeDeleteModal} size="sm">
|
<Button variant="default" onClick={closeDeleteModal} size="sm">
|
||||||
{t('location.cancel')}
|
{t('location.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="red" onClick={handleDeleteConfirm} size="sm">
|
<Button color="red" onClick={handleDeleteConfirm} size="sm">
|
||||||
{t('location.delete')}
|
{t('location.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<LocationTypeModal opened={typeModalOpened} onClose={closeTypeModal} />
|
<LocationTypeModal opened={typeModalOpened} onClose={closeTypeModal} />
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconLock,
|
IconLock,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify, ModalFooter } from '@ema-platform/ui';
|
||||||
import {
|
import {
|
||||||
extractErrorMessage,
|
extractErrorMessage,
|
||||||
localized,
|
localized,
|
||||||
@@ -364,14 +364,14 @@ function FeeEditModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Group justify="flex-end" mt="xs">
|
<ModalFooter mt="xs">
|
||||||
<Button variant="default" onClick={onClose} disabled={isLoading}>
|
<Button variant="default" onClick={onClose} disabled={isLoading}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={save} loading={isLoading}>
|
<Button onClick={save} loading={isLoading}>
|
||||||
Save fees
|
Save fees
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IconEdit, IconTrash, IconPlus, IconInfoCircle } from '@tabler/icons-react';
|
import { IconEdit, IconTrash, IconPlus, IconInfoCircle } from '@tabler/icons-react';
|
||||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
|
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from '@ema-platform/ui';
|
||||||
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
|
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
|
||||||
import {
|
import {
|
||||||
useGetQuestionsQuery,
|
useGetQuestionsQuery,
|
||||||
@@ -255,10 +255,10 @@ export function QuestionPage() {
|
|||||||
|
|
||||||
<Modal opened={deleteOpened} onClose={closeDelete} title={t('question.confirmDelete')} size="sm">
|
<Modal opened={deleteOpened} onClose={closeDelete} title={t('question.confirmDelete')} size="sm">
|
||||||
<Text mb="md">{t('question.deleteConfirmText')}</Text>
|
<Text mb="md">{t('question.deleteConfirmText')}</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closeDelete} size="sm">{t('question.cancel')}</Button>
|
<Button variant="default" onClick={closeDelete} size="sm">{t('question.cancel')}</Button>
|
||||||
<Button color="red" onClick={handleDelete} size="sm">{t('question.delete')}</Button>
|
<Button color="red" onClick={handleDelete} size="sm">{t('question.delete')}</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
|
import { IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
|
||||||
import { notify, useErrorHandler } from '@ema-platform/ui';
|
import { notify, useErrorHandler, ModalFooter } from '@ema-platform/ui';
|
||||||
import { useApiQuery } from '@ema-platform/api';
|
import { useApiQuery } from '@ema-platform/api';
|
||||||
import { useCreateResultMutation, useUpdateResultMutation } from '../api/result-api';
|
import { useCreateResultMutation, useUpdateResultMutation } from '../api/result-api';
|
||||||
import type { Exam } from '../../exam/types/exam';
|
import type { Exam } from '../../exam/types/exam';
|
||||||
@@ -195,12 +195,12 @@ export function RecordResultModal({
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={onClose} size="sm">{t('result.cancel')}</Button>
|
<Button variant="default" onClick={onClose} size="sm">{t('result.cancel')}</Button>
|
||||||
<Button onClick={handleSave} size="sm" loading={isSaving}>
|
<Button onClick={handleSave} size="sm" loading={isSaving}>
|
||||||
{t('result.saveResult')}
|
{t('result.saveResult')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
IconChartBar,
|
IconChartBar,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
|
import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from '@ema-platform/ui';
|
||||||
import type { BilingualValue } from '@ema-platform/ui';
|
import type { BilingualValue } from '@ema-platform/ui';
|
||||||
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation, useUpdateResultMutation } from '../api/result-api';
|
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation, useUpdateResultMutation } from '../api/result-api';
|
||||||
import { useGetExamsQuery } from '../../exam/api/exam-api';
|
import { useGetExamsQuery } from '../../exam/api/exam-api';
|
||||||
@@ -462,7 +462,7 @@ export function ResultPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={handleDetailClose} size="sm">{t('result.close')}</Button>
|
<Button variant="default" onClick={handleDetailClose} size="sm">{t('result.close')}</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDetailSave}
|
onClick={handleDetailSave}
|
||||||
@@ -472,7 +472,7 @@ export function ResultPage() {
|
|||||||
>
|
>
|
||||||
{t('result.save')}
|
{t('result.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Text c="dimmed" ta="center" py="xl">{t('result.noData')}</Text>
|
<Text c="dimmed" ta="center" py="xl">{t('result.noData')}</Text>
|
||||||
@@ -481,10 +481,10 @@ export function ResultPage() {
|
|||||||
|
|
||||||
<Modal opened={deleteOpened} onClose={closeDelete} title={t('result.confirmDelete')} size="sm">
|
<Modal opened={deleteOpened} onClose={closeDelete} title={t('result.confirmDelete')} size="sm">
|
||||||
<Text mb="md">{t('result.deleteConfirmText')}</Text>
|
<Text mb="md">{t('result.deleteConfirmText')}</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closeDelete} size="sm">{t('result.cancel')}</Button>
|
<Button variant="default" onClick={closeDelete} size="sm">{t('result.cancel')}</Button>
|
||||||
<Button color="red" onClick={handleDelete} size="sm">{t('result.delete')}</Button>
|
<Button color="red" onClick={handleDelete} size="sm">{t('result.delete')}</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Choose exam, then record */}
|
{/* Choose exam, then record */}
|
||||||
@@ -501,10 +501,10 @@ export function ResultPage() {
|
|||||||
searchable
|
searchable
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={closePicker} size="sm">{t('result.cancel')}</Button>
|
<Button variant="default" onClick={closePicker} size="sm">{t('result.cancel')}</Button>
|
||||||
<Button onClick={startRecord} size="sm" disabled={!pickerExamId}>{t('result.continue')}</Button>
|
<Button onClick={startRecord} size="sm" disabled={!pickerExamId}>{t('result.continue')}</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify, ModalFooter } from '@ema-platform/ui';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -495,12 +495,12 @@ export function VesselRegistrationFormBuilderPage() {
|
|||||||
onChange={(e) => setDraftRequired(e.currentTarget.checked)}
|
onChange={(e) => setDraftRequired(e.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={() => setDrawerOpen(false)}>Cancel</Button>
|
<Button variant="default" onClick={() => setDrawerOpen(false)}>Cancel</Button>
|
||||||
<Button color="teal" onClick={saveField}>
|
<Button color="teal" onClick={saveField}>
|
||||||
{isNew ? 'Add Field' : 'Save Changes'}
|
{isNew ? 'Add Field' : 'Save Changes'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
@@ -516,10 +516,10 @@ export function VesselRegistrationFormBuilderPage() {
|
|||||||
Are you sure you want to remove <strong>{deleteTarget?.label}</strong> from the form?
|
Are you sure you want to remove <strong>{deleteTarget?.label}</strong> from the form?
|
||||||
This cannot be undone.
|
This cannot be undone.
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="default" onClick={() => setDeleteTarget(null)}>Cancel</Button>
|
<Button variant="default" onClick={() => setDeleteTarget(null)}>Cancel</Button>
|
||||||
<Button color="red" onClick={confirmDelete}>Delete Field</Button>
|
<Button color="red" onClick={confirmDelete}>Delete Field</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
type FieldErrors,
|
type FieldErrors,
|
||||||
type ValidationIssue,
|
type ValidationIssue,
|
||||||
} from '@ema-platform/api';
|
} from '@ema-platform/api';
|
||||||
|
import { ModalFooter } from '@ema-platform/ui';
|
||||||
import { ConfigDrivenSection } from '../components/ConfigDrivenSection';
|
import { ConfigDrivenSection } from '../components/ConfigDrivenSection';
|
||||||
import { DocumentSlots } from '../components/DocumentSlots';
|
import { DocumentSlots } from '../components/DocumentSlots';
|
||||||
import { StaffEvidence } from '../components/StaffEvidence';
|
import { StaffEvidence } from '../components/StaffEvidence';
|
||||||
@@ -617,17 +618,19 @@ export function LicenseApplicationPage() {
|
|||||||
onChange={(v) => setNewStaff({ ...newStaff, yearsOfExperience: Number(v) || 0 })}
|
onChange={(v) => setNewStaff({ ...newStaff, yearsOfExperience: Number(v) || 0 })}
|
||||||
min={0}
|
min={0}
|
||||||
/>
|
/>
|
||||||
<Button
|
<ModalFooter>
|
||||||
onClick={async () => {
|
<Button
|
||||||
if (!newStaff.fullName.trim() || !staffModal) return;
|
onClick={async () => {
|
||||||
await addStaff({ id: appId, roleKey: staffModal, ...newStaff });
|
if (!newStaff.fullName.trim() || !staffModal) return;
|
||||||
setNewStaff({ fullName: '', position: '', yearsOfExperience: 0 });
|
await addStaff({ id: appId, roleKey: staffModal, ...newStaff });
|
||||||
setStaffModal(null);
|
setNewStaff({ fullName: '', position: '', yearsOfExperience: 0 });
|
||||||
refetch();
|
setStaffModal(null);
|
||||||
}}
|
refetch();
|
||||||
>
|
}}
|
||||||
Add
|
>
|
||||||
</Button>
|
Add
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
useGetMyOperatorTypesQuery,
|
useGetMyOperatorTypesQuery,
|
||||||
useUpdateMyOperatorTypesMutation,
|
useUpdateMyOperatorTypesMutation,
|
||||||
} from '@ema-platform/api';
|
} from '@ema-platform/api';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify, ModalFooter } from '@ema-platform/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The applicant's modes of operation — what they do, and therefore which
|
* The applicant's modes of operation — what they do, and therefore which
|
||||||
@@ -199,7 +199,7 @@ export function OperationsFormContent({
|
|||||||
Applications already filed carry on as they are, and licences
|
Applications already filed carry on as they are, and licences
|
||||||
already issued to you stay valid and can still be renewed.
|
already issued to you stay valid and can still be renewed.
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="flex-end" gap="sm">
|
<ModalFooter gap="sm">
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => setConfirmingRemoval(false)}
|
onClick={() => setConfirmingRemoval(false)}
|
||||||
@@ -209,7 +209,7 @@ export function OperationsFormContent({
|
|||||||
<Button color="orange" loading={saving} onClick={persist}>
|
<Button color="orange" loading={saving} onClick={persist}>
|
||||||
Remove and save
|
Remove and save
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import {
|
|||||||
IconX,
|
IconX,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { CountrySelect, notify, useErrorHandler } from '@ema-platform/ui';
|
import { CountrySelect, notify, useErrorHandler, ModalFooter } from '@ema-platform/ui';
|
||||||
import type { Seafarer } from './SeafarerRegistryPage';
|
import type { Seafarer } from './SeafarerRegistryPage';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -479,10 +479,10 @@ function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () =>
|
|||||||
<AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
|
<AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Select label="Status" data={['Approved', 'Pending', 'Expired']} defaultValue="Pending" />
|
<Select label="Status" data={['Approved', 'Pending', 'Expired']} defaultValue="Pending" />
|
||||||
<Group justify="flex-end" mt="sm">
|
<ModalFooter mt="sm">
|
||||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||||
<Button onClick={() => { notify.success('Training record added (demo).'); onClose(); }}>Save</Button>
|
<Button onClick={() => { notify.success('Training record added (demo).'); onClose(); }}>Save</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@@ -503,10 +503,10 @@ function AddMedicalModal({ opened, onClose }: { opened: boolean; onClose: () =>
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Select label="Result" data={['Fit', 'Unfit', 'Conditional']} defaultValue="Fit" />
|
<Select label="Result" data={['Fit', 'Unfit', 'Conditional']} defaultValue="Fit" />
|
||||||
<Textarea label="Remarks" placeholder="Any notes" autosize minRows={2} />
|
<Textarea label="Remarks" placeholder="Any notes" autosize minRows={2} />
|
||||||
<Group justify="flex-end" mt="sm">
|
<ModalFooter mt="sm">
|
||||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||||
<Button onClick={() => { notify.success('Medical record added (demo).'); onClose(); }}>Save</Button>
|
<Button onClick={() => { notify.success('Medical record added (demo).'); onClose(); }}>Save</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@@ -533,10 +533,10 @@ function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: ()
|
|||||||
<AmharicDatePicker label="To" value={toDate} onChange={setToDate} />
|
<AmharicDatePicker label="To" value={toDate} onChange={setToDate} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<TextInput label="Engagement Port" placeholder="Port name" />
|
<TextInput label="Engagement Port" placeholder="Port name" />
|
||||||
<Group justify="flex-end" mt="sm">
|
<ModalFooter mt="sm">
|
||||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||||
<Button onClick={() => { notify.success('Sea service record added (demo).'); onClose(); }}>Save</Button>
|
<Button onClick={() => { notify.success('Sea service record added (demo).'); onClose(); }}>Save</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@@ -560,10 +560,10 @@ function AddCertModal({ opened, onClose }: { opened: boolean; onClose: () => voi
|
|||||||
<AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
|
<AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Select label="Status" data={['Valid', 'Expired', 'Pending']} defaultValue="Valid" />
|
<Select label="Status" data={['Valid', 'Expired', 'Pending']} defaultValue="Valid" />
|
||||||
<Group justify="flex-end" mt="sm">
|
<ModalFooter mt="sm">
|
||||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||||
<Button onClick={() => { notify.success('Certification added (demo).'); onClose(); }}>Save</Button>
|
<Button onClick={() => { notify.success('Certification added (demo).'); onClose(); }}>Save</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -93,6 +93,22 @@ export const portalTheme = createTheme({
|
|||||||
Textarea: { defaultProps: { radius: 'md' } },
|
Textarea: { defaultProps: { radius: 'md' } },
|
||||||
Select: { defaultProps: { radius: 'md' } },
|
Select: { defaultProps: { radius: 'md' } },
|
||||||
PasswordInput: { defaultProps: { radius: 'md' } },
|
PasswordInput: { defaultProps: { radius: 'md' } },
|
||||||
|
// Mantine's stock scroll wrapper (NativeScrollArea) discards the
|
||||||
|
// max-height it's handed unless scrollAreaComponent is set, so a modal
|
||||||
|
// taller than the viewport just gets clipped with no way to scroll it.
|
||||||
|
// Making the body the scrollport here fixes every Modal/Drawer at once.
|
||||||
|
Modal: {
|
||||||
|
styles: {
|
||||||
|
content: { display: 'flex', flexDirection: 'column', maxHeight: '90dvh' },
|
||||||
|
body: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Drawer: {
|
||||||
|
styles: {
|
||||||
|
content: { display: 'flex', flexDirection: 'column' },
|
||||||
|
body: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto' },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
other: {
|
other: {
|
||||||
heroGradient: 'linear-gradient(135deg, #3160b7 0%, #1fc29d 100%)',
|
heroGradient: 'linear-gradient(135deg, #3160b7 0%, #1fc29d 100%)',
|
||||||
|
|||||||
@@ -31,6 +31,24 @@ export const emaTheme = createTheme({
|
|||||||
md: '0 4px 20px rgba(15,23,42,0.08)',
|
md: '0 4px 20px rgba(15,23,42,0.08)',
|
||||||
lg: '0 8px 30px rgba(15,23,42,0.12)',
|
lg: '0 8px 30px rgba(15,23,42,0.12)',
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
// Mantine's stock scroll wrapper (NativeScrollArea) discards the
|
||||||
|
// max-height it's handed unless scrollAreaComponent is set, so a modal
|
||||||
|
// taller than the viewport just gets clipped with no way to scroll it.
|
||||||
|
// Making the body the scrollport here fixes every Modal/Drawer at once.
|
||||||
|
Modal: {
|
||||||
|
styles: {
|
||||||
|
content: { display: 'flex', flexDirection: 'column', maxHeight: '90dvh' },
|
||||||
|
body: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Drawer: {
|
||||||
|
styles: {
|
||||||
|
content: { display: 'flex', flexDirection: 'column' },
|
||||||
|
body: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
other: {
|
other: {
|
||||||
heroGradient: 'linear-gradient(135deg, #3160b7 0%, #1fc29d 100%)',
|
heroGradient: 'linear-gradient(135deg, #3160b7 0%, #1fc29d 100%)',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./lib/input/BilingualInput";
|
export * from "./lib/input/BilingualInput";
|
||||||
export * from "./lib/feedback/ConfirmModal";
|
export * from "./lib/feedback/ConfirmModal";
|
||||||
|
export * from "./lib/feedback/ModalFooter";
|
||||||
export * from "./lib/feedback/ApiErrorAlert";
|
export * from "./lib/feedback/ApiErrorAlert";
|
||||||
export * from "./lib/feedback/notify";
|
export * from "./lib/feedback/notify";
|
||||||
export * from "./lib/feedback/FeatureUnavailable";
|
export * from "./lib/feedback/FeatureUnavailable";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Modal, Button, Group, Text } from '@mantine/core';
|
import { Modal, Button, Text } from '@mantine/core';
|
||||||
|
import { ModalFooter } from './ModalFooter';
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
interface ConfirmModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@@ -26,14 +27,14 @@ export function ConfirmModal({
|
|||||||
<Text size="sm" mb="xl">
|
<Text size="sm" mb="xl">
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="flex-end">
|
<ModalFooter>
|
||||||
<Button variant="subtle" onClick={onClose} disabled={loading}>
|
<Button variant="subtle" onClick={onClose} disabled={loading}>
|
||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="red" onClick={onConfirm} loading={loading}>
|
<Button color="red" onClick={onConfirm} loading={loading}>
|
||||||
{confirmLabel}
|
{confirmLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
38
libs/ui/src/lib/feedback/ModalFooter.tsx
Normal file
38
libs/ui/src/lib/feedback/ModalFooter.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Group, type GroupProps } from '@mantine/core';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action row pinned to the bottom of a modal.
|
||||||
|
*
|
||||||
|
* Modal bodies scroll (see the Modal styles in the theme), which would carry the
|
||||||
|
* confirm button off-screen on a tall modal. Sticking to the bottom of the
|
||||||
|
* scrollport keeps the decision reachable from any scroll position. Negative
|
||||||
|
* margins bleed the background over the body's padding so nothing shows through
|
||||||
|
* underneath; on a modal short enough not to scroll this renders identically to
|
||||||
|
* a plain Group.
|
||||||
|
*/
|
||||||
|
export function ModalFooter({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: Omit<GroupProps, 'style'> & { style?: CSSProperties }) {
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify="flex-end"
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: 'calc(var(--mb-padding, var(--mantine-spacing-md)) * -1)',
|
||||||
|
marginInline: 'calc(var(--mb-padding, var(--mantine-spacing-md)) * -1)',
|
||||||
|
marginBottom: 'calc(var(--mb-padding, var(--mantine-spacing-md)) * -1)',
|
||||||
|
padding: 'var(--mb-padding, var(--mantine-spacing-md))',
|
||||||
|
background: 'var(--mantine-color-body)',
|
||||||
|
borderTop: '1px solid var(--mantine-color-default-border)',
|
||||||
|
zIndex: 1,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user