diff --git a/src/BursarExportPlugin.test.tsx b/src/BursarExportPlugin.test.tsx new file mode 100644 index 00000000..ef3fcfc7 --- /dev/null +++ b/src/BursarExportPlugin.test.tsx @@ -0,0 +1,106 @@ +import { render, screen } from '@testing-library/react'; +import React, { ComponentType } from 'react'; +import BursarExportPlugin from './BursarExportPlugin'; +import { useStripes } from '@folio/stripes/core'; +import withIntlConfiguration from './test/util/withIntlConfiguration'; +import useInitialValues from './hooks/useInitialValues'; +import { Form, FormProps } from 'react-final-form'; +import arrayMutators from 'final-form-arrays'; +import { FORM_ID } from './form/ConfigurationForm'; +import userEvent from '@testing-library/user-event'; +import formValuesToDto from './api/dto/to/formValuesToDto'; +import schedulingToDto from './api/dto/to/schedulingToDto'; + +jest.mock('@folio/stripes/core', () => ({ + useStripes: jest.fn(), +})); +jest.mock('./api/mutators/useManualSchedulerMutation', () => () => jest.fn()); +jest.mock( + './api/mutators/useAutomaticSchedulerMutation', + () => () => jest.fn() +); +jest.mock('./hooks/useInitialValues', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('@folio/stripes/final-form', () => ({ + __esModule: true, + default: () => (Component: ComponentType) => (props: FormProps) => + ( +
+ {(formProps) => } + + ), +})); + +const feeFineOwner = { + owner: 'Test owner', + id: 'test_owner_id', +}; +const transferAccount = { + accountName: 'Test account', + ownerId: 'test_owner_id', + id: 'test_account_id', + desc: 'Test description', +}; + +jest.mock('./api/queries/useFeeFineOwners', () => ({ + __esModule: true, + default: () => ({ data: [feeFineOwner], isSuccess: true }), +})); + +jest.mock('./api/queries/useTransferAccounts', () => ({ + __esModule: true, + default: () => ({ data: [transferAccount], isSuccess: true }), +})); +jest.mock('./api/dto/to/formValuesToDto', () => jest.fn()); +jest.mock('./api/dto/to/schedulingToDto', () => jest.fn()); + +describe('BursarExportPlugin', () => { + it('renders the plugin with null initial values', () => { + (useInitialValues as jest.Mock).mockReturnValue(null); + + render(withIntlConfiguration()); + + expect(screen.getByText('Transfer configuration')).toBeVisible(); + expect(document.getElementById(FORM_ID)).toBeNull(); + }); + + it('fills out the form and then saves and runs the plugin', async () => { + (useInitialValues as jest.Mock).mockReturnValue({ aggregate: false }); + (useStripes as jest.Mock).mockReturnValue({ hasPerm: () => true }); + + render(withIntlConfiguration()); + + expect(screen.getByText('Transfer configuration')).toBeVisible(); + expect(document.getElementById(FORM_ID)).not.toBeNull(); + expect(screen.getByText('Account data format')).toBeVisible(); + expect(screen.getByText('Save')).toBeVisible(); + + expect(screen.queryByText('Transfer to:')).not.toBeNull(); + + const frequencyDropdown = document.querySelector( + '[name = "scheduling.frequency"]' + ) as HTMLSelectElement; + await userEvent.selectOptions(frequencyDropdown, 'NONE'); + + const ownerDropdown = document.querySelector( + '[name = "transferInfo.else.owner"]' + ) as HTMLSelectElement; + await userEvent.selectOptions(ownerDropdown, 'test_owner_id'); + + const accountDropdown = document.querySelector( + '[name = "transferInfo.else.account"]' + ) as HTMLSelectElement; + await userEvent.selectOptions(accountDropdown, 'test_account_id'); + + await userEvent.click(screen.getByText('Save')); + + expect(formValuesToDto).toHaveBeenCalled(); + expect(schedulingToDto).toHaveBeenCalled(); + + await userEvent.click(screen.getByText('Run manually')); + + expect(formValuesToDto).toHaveBeenCalled(); + }); +}); diff --git a/src/BursarExportPlugin.tsx b/src/BursarExportPlugin.tsx index a47dc6fd..cfbdc08d 100644 --- a/src/BursarExportPlugin.tsx +++ b/src/BursarExportPlugin.tsx @@ -14,6 +14,7 @@ import useManualSchedulerMutation from './api/mutators/useManualSchedulerMutatio import ConfigurationForm, { FORM_ID } from './form/ConfigurationForm'; import useInitialValues from './hooks/useInitialValues'; import FormValues from './types/FormValues'; +import { FormattedMessage } from 'react-intl'; export default function BursarExportPlugin() { const stripes = useStripes(); @@ -39,14 +40,20 @@ export default function BursarExportPlugin() { if (initialValues === null) { return ( + } defaultWidth="fill" footer={ Run manually} + renderStart={ + + } renderEnd={ } /> @@ -69,7 +76,7 @@ export default function BursarExportPlugin() { formApiRef.current?.change('buttonClicked', 'manual') } > - Run manually + } renderEnd={ @@ -82,13 +89,15 @@ export default function BursarExportPlugin() { formApiRef.current?.change('buttonClicked', 'save') } > - Save + } /> } id="pane-batch-group-configuration" - paneTitle="Transfer configuration" + paneTitle={ + + } > ('aggregateFilter.type', { @@ -12,9 +13,33 @@ export default function AggregateCriteriaCard() { }).input.value; const monetaryOnBlur = useMonetaryOnBlur('aggregateFilter.amountDollars'); + const intl = useIntl(); + + const criteriaOptions = useMemo( + () => + [ + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.aggregate.filter.numAccounts', + }), + value: CriteriaAggregateType.NUM_ROWS, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.aggregate.filter.totalAmount', + }), + value: CriteriaAggregateType.TOTAL_AMOUNT, + }, + ].sort((a, b) => a.label.localeCompare(b.label)), + [intl] + ); return ( - + + } + > + } dataOptions={[ { - label: 'None (include all patrons)', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.aggregate.filter.none', + }), value: CriteriaAggregateType.PASS, }, - { - label: 'Number of accounts', - value: CriteriaAggregateType.NUM_ROWS, - }, - { - label: 'Total amount', - value: CriteriaAggregateType.TOTAL_AMOUNT, - }, + ...criteriaOptions, ]} /> )} @@ -63,7 +85,9 @@ export default function AggregateCriteriaCard() { marginBottom0 required type="number" - label="Number of accounts" + label={ + + } min={1} step={1} /> @@ -82,7 +106,9 @@ export default function AggregateCriteriaCard() { marginBottom0 required type="number" - label="Amount" + label={ + + } min={0} step={0.01} onBlur={monetaryOnBlur} @@ -95,8 +121,7 @@ export default function AggregateCriteriaCard() {

- This will be applied after accounts are evaluated per the - “Criteria” specified above. +

diff --git a/src/components/Criteria/CriteriaAge.tsx b/src/components/Criteria/CriteriaAge.tsx index f1afd28c..504752dc 100644 --- a/src/components/Criteria/CriteriaAge.tsx +++ b/src/components/Criteria/CriteriaAge.tsx @@ -1,6 +1,7 @@ import { Col, TextField } from '@folio/stripes/components'; import React from 'react'; import { Field } from 'react-final-form'; +import { FormattedMessage } from 'react-intl'; export default function CriteriaAge({ prefix }: { prefix: string }) { return ( @@ -13,7 +14,9 @@ export default function CriteriaAge({ prefix }: { prefix: string }) { marginBottom0 required type="number" - label="Older than (days)" + label={ + + } min={1} step={1} /> diff --git a/src/components/Criteria/CriteriaAmount.tsx b/src/components/Criteria/CriteriaAmount.tsx index 23fa1acd..a239fd27 100644 --- a/src/components/Criteria/CriteriaAmount.tsx +++ b/src/components/Criteria/CriteriaAmount.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { Field } from 'react-final-form'; import useMonetaryOnBlur from '../../hooks/useMonetaryOnBlur'; import OperatorSelect from './OperatorSelect'; +import { FormattedMessage } from 'react-intl'; export default function CriteriaAmount({ prefix }: { prefix: string }) { const monetaryOnBlur = useMonetaryOnBlur(`${prefix}amountDollars`); @@ -21,7 +22,9 @@ export default function CriteriaAmount({ prefix }: { prefix: string }) { marginBottom0 required type="number" - label="Amount" + label={ + + } min={0} step={0.01} onBlur={monetaryOnBlur} diff --git a/src/components/Criteria/CriteriaCardSelect.test.tsx b/src/components/Criteria/CriteriaCardSelect.test.tsx index 0711cf8b..03d0d4e9 100644 --- a/src/components/Criteria/CriteriaCardSelect.test.tsx +++ b/src/components/Criteria/CriteriaCardSelect.test.tsx @@ -2,13 +2,16 @@ import { render, screen } from '@testing-library/react'; import { Form } from 'react-final-form'; import CriteriaCardSelect from './CriteriaCardSelect'; import React from 'react'; +import withIntlConfiguration from '../../test/util/withIntlConfiguration'; describe('Criteria card selection box', () => { it('root has no criteria option', () => { render( -
- {() => } - + withIntlConfiguration( +
+ {() => } + + ) ); expect( @@ -18,9 +21,11 @@ describe('Criteria card selection box', () => { it('non patron-only has item/etc options', () => { render( -
- {() => } - + withIntlConfiguration( +
+ {() => } + + ) ); expect(screen.getByRole('option', { name: 'All of:' })).toBeInTheDocument(); @@ -34,9 +39,11 @@ describe('Criteria card selection box', () => { it('patron-only does not have item/etc options', () => { render( -
- {() => } - + withIntlConfiguration( +
+ {() => } + + ) ); expect(screen.getByRole('option', { name: 'All of:' })).toBeInTheDocument(); diff --git a/src/components/Criteria/CriteriaCardSelect.tsx b/src/components/Criteria/CriteriaCardSelect.tsx index 2bec11bf..87639cd2 100644 --- a/src/components/Criteria/CriteriaCardSelect.tsx +++ b/src/components/Criteria/CriteriaCardSelect.tsx @@ -5,6 +5,7 @@ import { CriteriaGroupType, CriteriaTerminalType, } from '../../types/CriteriaTypes'; +import { useIntl } from 'react-intl'; export default function CriteriaCardSelect({ name, @@ -23,20 +24,28 @@ export default function CriteriaCardSelect({ } }, [root]); + const intl = useIntl(); + const selectOptions = useMemo(() => { const options: SelectOptionType< CriteriaGroupType | CriteriaTerminalType >[] = [ { - label: 'All of:', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.allOf', + }), value: CriteriaGroupType.ALL_OF, }, { - label: 'Any of:', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.anyOf', + }), value: CriteriaGroupType.ANY_OF, }, { - label: 'None of:', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.noneOf', + }), value: CriteriaGroupType.NONE_OF, }, @@ -46,44 +55,67 @@ export default function CriteriaCardSelect({ disabled: true, }, - // TODO: sort these alphabetically per i18n ...(patronOnly - ? [] + ? [ + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.patronGroup', + }), + value: CriteriaTerminalType.PATRON_GROUP, + }, + ] : [ { - label: 'Age', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.age', + }), value: CriteriaTerminalType.AGE, }, { - label: 'Amount', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.amount', + }), value: CriteriaTerminalType.AMOUNT, }, { - label: 'Fee/fine owner', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.owner', + }), value: CriteriaTerminalType.FEE_FINE_OWNER, }, { - label: 'Fee/fine type', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.type', + }), value: CriteriaTerminalType.FEE_FINE_TYPE, }, { - label: 'Item location', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.location', + }), value: CriteriaTerminalType.LOCATION, }, { - label: 'Item service point', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.servicePoint', + }), value: CriteriaTerminalType.SERVICE_POINT, }, - ]), - { - label: 'Patron group', - value: CriteriaTerminalType.PATRON_GROUP, - }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.patronGroup', + }), + value: CriteriaTerminalType.PATRON_GROUP, + }, + ] + ).sort((a, b) => a.label.localeCompare(b.label)), ]; if (root) { options.unshift({ - label: 'No criteria (always run)', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.select.none', + }), value: CriteriaTerminalType.PASS, }); } @@ -92,7 +124,13 @@ export default function CriteriaCardSelect({ }, [root, patronOnly]); return ( - + {(fieldProps) => ( {...fieldProps} diff --git a/src/components/Criteria/CriteriaFeeFineOwner.tsx b/src/components/Criteria/CriteriaFeeFineOwner.tsx index a024f384..8f3d38c9 100644 --- a/src/components/Criteria/CriteriaFeeFineOwner.tsx +++ b/src/components/Criteria/CriteriaFeeFineOwner.tsx @@ -2,6 +2,7 @@ import { Col, Select } from '@folio/stripes/components'; import React, { useMemo } from 'react'; import { Field } from 'react-final-form'; import useFeeFineOwners from '../../api/queries/useFeeFineOwners'; +import { FormattedMessage } from 'react-intl'; export default function CriteriaFeeFineOwner({ prefix }: { prefix: string }) { const feeFineOwners = useFeeFineOwners(); @@ -11,10 +12,12 @@ export default function CriteriaFeeFineOwner({ prefix }: { prefix: string }) { return []; } - return feeFineOwners.data.map((owner) => ({ - label: owner.owner, - value: owner.id, - })); + return feeFineOwners.data + .map((owner) => ({ + label: owner.owner, + value: owner.id, + })) + .sort((a, b) => a.label.localeCompare(b.label)); }, [feeFineOwners]); return ( @@ -27,7 +30,9 @@ export default function CriteriaFeeFineOwner({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Fee/fine owner" + label={ + + } dataOptions={[ { label: '', value: '', disabled: true }, ...ownersSelectOptions, diff --git a/src/components/Criteria/CriteriaFeeFineType.tsx b/src/components/Criteria/CriteriaFeeFineType.tsx index 330d487a..a7ca6bd1 100644 --- a/src/components/Criteria/CriteriaFeeFineType.tsx +++ b/src/components/Criteria/CriteriaFeeFineType.tsx @@ -3,10 +3,12 @@ import React, { useMemo } from 'react'; import { Field, useField } from 'react-final-form'; import useFeeFineOwners from '../../api/queries/useFeeFineOwners'; import useFeeFineTypes from '../../api/queries/useFeeFineTypes'; +import { FormattedMessage, useIntl } from 'react-intl'; export default function CriteriaFeeFineType({ prefix }: { prefix: string }) { const feeFineOwners = useFeeFineOwners(); const feeFineTypes = useFeeFineTypes(); + const intl = useIntl(); const selectedOwner = useField( `${prefix}feeFineOwnerId`, @@ -60,11 +62,18 @@ export default function CriteriaFeeFineType({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Fee/fine owner" + label={ + + } dataOptions={[ - { label: 'Automatic', value: 'automatic' }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.criteria.type.automatic', + }), + value: 'automatic', + }, ...ownersSelectOptions, - ]} + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} @@ -77,11 +86,13 @@ export default function CriteriaFeeFineType({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Fee/fine type" + label={ + + } dataOptions={[ { label: '', value: undefined }, ...typeSelectOptions, - ]} + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} diff --git a/src/components/Criteria/CriteriaLocation.tsx b/src/components/Criteria/CriteriaLocation.tsx index e596721c..aeee14d9 100644 --- a/src/components/Criteria/CriteriaLocation.tsx +++ b/src/components/Criteria/CriteriaLocation.tsx @@ -5,6 +5,7 @@ import useCampuses from '../../api/queries/useCampuses'; import useInstitutions from '../../api/queries/useInstitutions'; import useLibraries from '../../api/queries/useLibraries'; import useLocations from '../../api/queries/useLocations'; +import { FormattedMessage } from 'react-intl'; export default function CriteriaLocation({ prefix }: { prefix: string }) { const institutions = useInstitutions(); @@ -85,11 +86,13 @@ export default function CriteriaLocation({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Institution" + label={ + + } dataOptions={[ { label: '', value: undefined }, ...institutionSelectOptions, - ]} + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} @@ -102,11 +105,13 @@ export default function CriteriaLocation({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Campus" + label={ + + } dataOptions={[ { label: '', value: undefined }, ...campusSelectOptions, - ]} + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} @@ -119,11 +124,13 @@ export default function CriteriaLocation({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Library" + label={ + + } dataOptions={[ { label: '', value: undefined }, ...librarySelectOptions, - ]} + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} @@ -136,11 +143,13 @@ export default function CriteriaLocation({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Location" + label={ + + } dataOptions={[ { label: '', value: undefined }, ...locationSelectOptions, - ]} + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} diff --git a/src/components/Criteria/CriteriaPatronGroup.tsx b/src/components/Criteria/CriteriaPatronGroup.tsx index ca28cc4e..9284f1cb 100644 --- a/src/components/Criteria/CriteriaPatronGroup.tsx +++ b/src/components/Criteria/CriteriaPatronGroup.tsx @@ -2,6 +2,7 @@ import { Col, Select } from '@folio/stripes/components'; import React, { useMemo } from 'react'; import { Field } from 'react-final-form'; import usePatronGroups from '../../api/queries/usePatronGroups'; +import { FormattedMessage } from 'react-intl'; export default function CriteriaPatronGroup({ prefix }: { prefix: string }) { const patronGroups = usePatronGroups(); @@ -26,8 +27,13 @@ export default function CriteriaPatronGroup({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Patron group" - dataOptions={[{ label: '', value: undefined }, ...selectOptions]} + label={ + + } + dataOptions={[ + { label: '', value: undefined }, + ...selectOptions, + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} diff --git a/src/components/Criteria/CriteriaServicePoint.tsx b/src/components/Criteria/CriteriaServicePoint.tsx index 710da195..39ba8a25 100644 --- a/src/components/Criteria/CriteriaServicePoint.tsx +++ b/src/components/Criteria/CriteriaServicePoint.tsx @@ -2,6 +2,7 @@ import { Col, Select } from '@folio/stripes/components'; import React, { useMemo } from 'react'; import { Field } from 'react-final-form'; import useServicePoints from '../../api/queries/useServicePoints'; +import { FormattedMessage } from 'react-intl'; export default function CriteriaServicePoint({ prefix }: { prefix: string }) { const servicePoints = useServicePoints(); @@ -26,8 +27,13 @@ export default function CriteriaServicePoint({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Service point" - dataOptions={[{ label: '', value: undefined }, ...selectOptions]} + label={ + + } + dataOptions={[ + { label: '', value: undefined }, + ...selectOptions, + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} diff --git a/src/components/Criteria/OperatorSelect.tsx b/src/components/Criteria/OperatorSelect.tsx index b38c7836..5d6ba999 100644 --- a/src/components/Criteria/OperatorSelect.tsx +++ b/src/components/Criteria/OperatorSelect.tsx @@ -2,8 +2,10 @@ import { Select } from '@folio/stripes/components'; import React from 'react'; import { Field } from 'react-final-form'; import { ComparisonOperator } from '../../types/CriteriaTypes'; +import { FormattedMessage, useIntl } from 'react-intl'; export default function OperatorSelect({ name }: { name: string }) { + const intl = useIntl(); return ( {(fieldProps) => ( @@ -12,23 +14,33 @@ export default function OperatorSelect({ name }: { name: string }) { fullWidth marginBottom0 required - label="Comparison operator" + label={ + + } dataOptions={[ { label: '', value: undefined }, { - label: 'Less than but not equal to', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.aggregate.filter.operator.less', + }), value: ComparisonOperator.LESS_THAN, }, { - label: 'Less than or equal to', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.aggregate.filter.operator.lessEqual', + }), value: ComparisonOperator.LESS_THAN_EQUAL, }, { - label: 'Greater than but not equal to', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.aggregate.filter.operator.greater', + }), value: ComparisonOperator.GREATER_THAN, }, { - label: 'Greater than or equal to', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.aggregate.filter.operator.greaterEqual', + }), value: ComparisonOperator.GREATER_THAN_EQUAL, }, ]} diff --git a/src/components/Token/Data/AccountDateToken.tsx b/src/components/Token/Data/AccountDateToken.tsx index 2349c54b..5202e761 100644 --- a/src/components/Token/Data/AccountDateToken.tsx +++ b/src/components/Token/Data/AccountDateToken.tsx @@ -3,8 +3,10 @@ import React from 'react'; import { Field } from 'react-final-form'; import DatePartPicker from '../Shared/DatePartPicker'; import TimezonePicker from '../Shared/TimezonePicker'; +import { FormattedMessage, useIntl } from 'react-intl'; export default function AccountDateToken({ prefix }: { prefix: string }) { + const intl = useIntl(); return ( <> @@ -19,19 +21,27 @@ export default function AccountDateToken({ prefix }: { prefix: string }) { label="Date" dataOptions={[ { - label: 'Creation date', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.accountDate.dateType.created', + }), value: 'CREATED', }, { - label: 'Last updated date', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.accountDate.dateType.updated', + }), value: 'UPDATED', }, { - label: 'Item due date', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.accountDate.dateType.dueItem', + }), value: 'DUE', }, { - label: 'Loan end date', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.accountDate.dateType.dueLoan', + }), value: 'DUE', }, ]} @@ -48,7 +58,9 @@ export default function AccountDateToken({ prefix }: { prefix: string }) { {...fieldProps} fullWidth - label="Fallback value" + label={ + + } /> )} @@ -59,8 +71,7 @@ export default function AccountDateToken({ prefix }: { prefix: string }) {

- If the chosen date is not available/applicable, the fallback value - will be used instead. +

diff --git a/src/components/Token/Data/ConstantConditionalToken.tsx b/src/components/Token/Data/ConstantConditionalToken.tsx index 3321393c..3c7eae55 100644 --- a/src/components/Token/Data/ConstantConditionalToken.tsx +++ b/src/components/Token/Data/ConstantConditionalToken.tsx @@ -9,6 +9,7 @@ import { CriteriaTerminal, } from '../../../types/CriteriaTypes'; import ConditionalCard from '../../ConditionalCard'; +import { FormattedMessage } from 'react-intl'; export default function ConstantConditionalToken({ prefix, @@ -42,7 +43,9 @@ export default function ConstantConditionalToken({ {...fieldProps} fullWidth required - label="Then use:" + label={ + + } /> )} @@ -50,14 +53,20 @@ export default function ConstantConditionalToken({ ))} - + + } + > {(fieldProps) => ( {...fieldProps} fullWidth required - label="Fallback value" + label={ + + } /> )} @@ -67,13 +76,11 @@ export default function ConstantConditionalToken({

- Conditions will be evaluated in order, with the first matched - value being used. If no conditions are matched, the fallback - value will be used. +

diff --git a/src/components/Token/Data/DataTypeSelect.tsx b/src/components/Token/Data/DataTypeSelect.tsx index 7f42e505..fd9fa4b6 100644 --- a/src/components/Token/Data/DataTypeSelect.tsx +++ b/src/components/Token/Data/DataTypeSelect.tsx @@ -1,108 +1,166 @@ import { Select } from '@folio/stripes/components'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Field, useField } from 'react-final-form'; import { DataTokenType } from '../../../types/TokenTypes'; +import { useIntl } from 'react-intl'; -const ALWAYS_AVAILABLE_OPTIONS = [ - { - label: 'Newline (LF)', - value: DataTokenType.NEWLINE, - }, - { - label: 'Newline (Microsoft, CRLF)', - value: DataTokenType.NEWLINE_MICROSOFT, - }, - { - label: 'Tab', - value: DataTokenType.TAB, - }, - { - label: 'Comma', - value: DataTokenType.COMMA, - }, - { - label: 'Whitespace', - value: DataTokenType.SPACE, - }, - - { - label: '', - value: DataTokenType.NEWLINE, - disabled: true, - }, +export default function DataTypeSelect({ name }: { name: string }) { + const isAggregate = useField('aggregate', { + subscription: { value: true }, + format: (value) => value ?? false, + }).input.value; - { - label: 'Arbitrary text', - value: DataTokenType.ARBITRARY_TEXT, - }, - { - label: 'Current date', - value: DataTokenType.CURRENT_DATE, - }, - { - label: 'Conditional text', - value: DataTokenType.CONSTANT_CONDITIONAL, - }, + const intl = useIntl(); - { - label: '', - value: DataTokenType.NEWLINE, - disabled: true, - }, + const alwaysAvailableOptions = useMemo(() => { + const topOptions = [ + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.newline', + }), + value: DataTokenType.NEWLINE, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.newlineMicrosoft', + }), + value: DataTokenType.NEWLINE_MICROSOFT, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.tab', + }), + value: DataTokenType.TAB, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.comma', + }), + value: DataTokenType.COMMA, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.whitespace', + }), + value: DataTokenType.SPACE, + }, + ].sort((a, b) => a.label.localeCompare(b.label)); - { - label: 'User info', - value: DataTokenType.USER_DATA, - }, -]; + const bottomOptions = [ + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.arbitraryText', + }), + value: DataTokenType.ARBITRARY_TEXT, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate', + }), + value: DataTokenType.CURRENT_DATE, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.constantConditional', + }), + value: DataTokenType.CONSTANT_CONDITIONAL, + }, + ].sort((a, b) => a.label.localeCompare(b.label)); -const NON_AGGREGATE_OPTIONS = [ - { - label: 'Account amount', - value: DataTokenType.ACCOUNT_AMOUNT, - }, - { - label: 'Account date', - value: DataTokenType.ACCOUNT_DATE, - }, - { - label: 'Fee/fine type', - value: DataTokenType.FEE_FINE_TYPE, - }, - { - label: 'Item info', - value: DataTokenType.ITEM_INFO, - }, -]; + return [ + ...topOptions, + { + label: '', + value: DataTokenType.NEWLINE, + disabled: true, + }, + ...bottomOptions, + { + label: '', + value: DataTokenType.NEWLINE, + disabled: true, + }, + ]; + }, [intl]); -const AGGREGATE_OPTIONS = [ - { - label: 'Total amount', - value: DataTokenType.AGGREGATE_TOTAL, - }, - { - label: 'Number of accounts', - value: DataTokenType.AGGREGATE_COUNT, - }, -]; + const noneAggregateOptions = useMemo( + () => + [ + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.userData', + }), + value: DataTokenType.USER_DATA, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.accountAmount', + }), + value: DataTokenType.ACCOUNT_AMOUNT, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.accountDate', + }), + value: DataTokenType.ACCOUNT_DATE, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.feeFineType', + }), + value: DataTokenType.FEE_FINE_TYPE, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.itemInfo', + }), + value: DataTokenType.ITEM_INFO, + }, + ].sort((a, b) => a.label.localeCompare(b.label)), + [intl] + ); -export default function DataTypeSelect({ name }: { name: string }) { - const isAggregate = useField('aggregate', { - subscription: { value: true }, - format: (value) => value ?? false, - }).input.value; + const aggregateOptions = useMemo( + () => + [ + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.userData', + }), + value: DataTokenType.USER_DATA, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.totalAmount', + }), + value: DataTokenType.AGGREGATE_TOTAL, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.numAccounts', + }), + value: DataTokenType.AGGREGATE_COUNT, + }, + ].sort((a, b) => a.label.localeCompare(b.label)), + [intl] + ); return ( - + {(fieldProps) => ( {...fieldProps} required marginBottom0 dataOptions={[ - // TODO: sort these alphabetically per i18n - ...ALWAYS_AVAILABLE_OPTIONS, - ...(isAggregate ? AGGREGATE_OPTIONS : NON_AGGREGATE_OPTIONS), + ...alwaysAvailableOptions, + ...(isAggregate ? aggregateOptions : noneAggregateOptions), ]} /> )} diff --git a/src/components/Token/Data/FeeFineTypeToken.test.tsx b/src/components/Token/Data/FeeFineTypeToken.test.tsx index 29f253c5..70b6d093 100644 --- a/src/components/Token/Data/FeeFineTypeToken.test.tsx +++ b/src/components/Token/Data/FeeFineTypeToken.test.tsx @@ -4,23 +4,26 @@ import React from 'react'; import { Form } from 'react-final-form'; import { DataTokenType } from '../../../types/TokenTypes'; import DataTokenCardBody from './DataTokenCardBody'; +import withIntlConfiguration from '../../../test/util/withIntlConfiguration'; describe('Fee/fine type token', () => { it('displays appropriate form', async () => { const submitter = jest.fn(); render( -
submitter(v)} - initialValues={{ test: { type: DataTokenType.FEE_FINE_TYPE } }} - > - {({ handleSubmit }) => ( - - - - - )} - + withIntlConfiguration( +
submitter(v)} + initialValues={{ test: { type: DataTokenType.FEE_FINE_TYPE } }} + > + {({ handleSubmit }) => ( + + + + + )} + + ) ); await userEvent.click(screen.getByRole('button', { name: 'Submit' })); diff --git a/src/components/Token/Data/FeeFineTypeToken.tsx b/src/components/Token/Data/FeeFineTypeToken.tsx index 9a546dcc..d1522c5c 100644 --- a/src/components/Token/Data/FeeFineTypeToken.tsx +++ b/src/components/Token/Data/FeeFineTypeToken.tsx @@ -1,8 +1,10 @@ import { Col, Select } from '@folio/stripes/components'; import React from 'react'; import { Field } from 'react-final-form'; +import { FormattedMessage, useIntl } from 'react-intl'; export default function FeeFineTypeToken({ prefix }: { prefix: string }) { + const intl = useIntl(); return ( @@ -14,14 +16,20 @@ export default function FeeFineTypeToken({ prefix }: { prefix: string }) { {...fieldProps} required marginBottom0 - label="Attribute" + label={ + + } dataOptions={[ { - label: 'Type name', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.feeFineType.name', + }), value: 'FEE_FINE_TYPE_NAME', }, { - label: 'Type ID', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.feeFineType.id', + }), value: 'FEE_FINE_TYPE_ID', }, ]} diff --git a/src/components/Token/Data/ItemInfoToken.tsx b/src/components/Token/Data/ItemInfoToken.tsx index 6c55bdbe..1c50bf59 100644 --- a/src/components/Token/Data/ItemInfoToken.tsx +++ b/src/components/Token/Data/ItemInfoToken.tsx @@ -1,43 +1,57 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { ItemAttribute } from '../../../types/TokenTypes'; import UserItemInfoToken from './UserItemInfoToken'; +import { useIntl } from 'react-intl'; export default function ItemInfoToken({ prefix }: { prefix: string }) { + const intl = useIntl(); + const attributeOptions = [ + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.itemInfo.name', + value: 'NAME', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.itemInfo.barcode', + value: 'BARCODE', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.itemInfo.material', + value: 'MATERIAL_TYPE', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.itemInfo.instId', + value: 'INSTITUTION_ID', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.itemInfo.campId', + value: 'CAMPUS_ID', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.itemInfo.libId', + value: 'LIBRARY_ID', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.itemInfo.locId', + value: 'LOCATION_ID', + }, + ]; + const options = useMemo( + () => + attributeOptions + .map((option) => ({ + label: intl.formatMessage({ id: option.labelId }), + value: option.value as ItemAttribute, + })) + .sort((a, b) => a.label.localeCompare(b.label)), + [intl] + ); + return ( prefix={prefix} defaultValue="NAME" attributeName="itemAttribute" - options={[ - { - label: 'Name', - value: 'NAME', - }, - { - label: 'Barcode', - value: 'BARCODE', - }, - { - label: 'Material type', - value: 'MATERIAL_TYPE', - }, - { - label: 'Institution ID', - value: 'INSTITUTION_ID', - }, - { - label: 'Campus ID', - value: 'CAMPUS_ID', - }, - { - label: 'Library ID', - value: 'LIBRARY_ID', - }, - { - label: 'Location ID', - value: 'LOCATION_ID', - }, - ]} + options={options} /> ); } diff --git a/src/components/Token/Data/UserInfoToken.tsx b/src/components/Token/Data/UserInfoToken.tsx index fbe21ed2..bb9cd613 100644 --- a/src/components/Token/Data/UserInfoToken.tsx +++ b/src/components/Token/Data/UserInfoToken.tsx @@ -1,47 +1,61 @@ -import React from 'react'; -import { UserAttribute } from '../../../types/TokenTypes'; +import React, { useMemo } from 'react'; import UserItemInfoToken from './UserItemInfoToken'; +import { useIntl } from 'react-intl'; +import { UserAttribute } from '../../../types/TokenTypes'; export default function UserInfoToken({ prefix }: { prefix: string }) { + const intl = useIntl(); + + const attributeOptions = [ + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.userInfo.folioId', + value: 'FOLIO_ID', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.userInfo.extId', + value: 'EXTERNAL_SYSTEM_ID', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.userInfo.groupId', + value: 'PATRON_GROUP_ID', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.userInfo.barcode', + value: 'BARCODE', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.userInfo.username', + value: 'USERNAME', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.userInfo.firstname', + value: 'FIRST_NAME', + }, + { + labelId: + 'ui-plugin-bursar-export.bursarExports.token.userInfo.middlename', + value: 'MIDDLE_NAME', + }, + { + labelId: 'ui-plugin-bursar-export.bursarExports.token.userInfo.lastname', + value: 'LAST_NAME', + }, + ]; + const options = useMemo( + () => + attributeOptions.map((option) => ({ + label: intl.formatMessage({ id: option.labelId }), + value: option.value as UserAttribute, + })), + [intl] + ); + return ( prefix={prefix} defaultValue="EXTERNAL_SYSTEM_ID" attributeName="userAttribute" - options={[ - { - label: 'Folio ID', - value: 'FOLIO_ID', - }, - { - label: 'External system ID', - value: 'EXTERNAL_SYSTEM_ID', - }, - { - label: 'Patron group ID', - value: 'PATRON_GROUP_ID', - }, - { - label: 'Barcode', - value: 'BARCODE', - }, - { - label: 'Username', - value: 'USERNAME', - }, - { - label: 'First name', - value: 'FIRST_NAME', - }, - { - label: 'Middle name', - value: 'MIDDLE_NAME', - }, - { - label: 'Last name', - value: 'LAST_NAME', - }, - ]} + options={options} /> ); } diff --git a/src/components/Token/Data/UserItemInfoToken.tsx b/src/components/Token/Data/UserItemInfoToken.tsx index 1339c462..75c54320 100644 --- a/src/components/Token/Data/UserItemInfoToken.tsx +++ b/src/components/Token/Data/UserItemInfoToken.tsx @@ -7,6 +7,7 @@ import { import React from 'react'; import { Field } from 'react-final-form'; import { ItemAttribute, UserAttribute } from '../../../types/TokenTypes'; +import { FormattedMessage } from 'react-intl'; export default function UserItemInfoToken< T extends ItemAttribute | UserAttribute @@ -32,7 +33,9 @@ export default function UserItemInfoToken< {...fieldProps} required - label="Value" + label={ + + } dataOptions={options} /> )} @@ -44,7 +47,9 @@ export default function UserItemInfoToken< {...fieldProps} fullWidth - label="Fallback value" + label={ + + } /> )}
@@ -52,8 +57,7 @@ export default function UserItemInfoToken<

- If the chosen value is not available/applicable, the fallback value - will be used instead. +

diff --git a/src/components/Token/HeaderFooter/HeaderFooterTypeSelect.tsx b/src/components/Token/HeaderFooter/HeaderFooterTypeSelect.tsx index f760562a..d5eb9ac3 100644 --- a/src/components/Token/HeaderFooter/HeaderFooterTypeSelect.tsx +++ b/src/components/Token/HeaderFooter/HeaderFooterTypeSelect.tsx @@ -1,62 +1,100 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { HeaderFooterTokenType } from '../../../types/TokenTypes'; import { Select } from '@folio/stripes/components'; import { Field } from 'react-final-form'; +import { useIntl } from 'react-intl'; export default function HeaderFooterTypeSelect({ name }: { name: string }) { + const intl = useIntl(); + const options = useMemo(() => { + const topSection = [ + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.newline', + }), + value: HeaderFooterTokenType.NEWLINE, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.newlineMicrosoft', + }), + value: HeaderFooterTokenType.NEWLINE_MICROSOFT, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.tab', + }), + value: HeaderFooterTokenType.TAB, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.comma', + }), + value: HeaderFooterTokenType.COMMA, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.whitespace', + }), + value: HeaderFooterTokenType.SPACE, + }, + ]; + + const bottomSection = [ + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.arbitraryText', + }), + value: HeaderFooterTokenType.ARBITRARY_TEXT, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate', + }), + value: HeaderFooterTokenType.CURRENT_DATE, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.numAccounts', + }), + value: HeaderFooterTokenType.AGGREGATE_COUNT, + }, + { + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.totalAmount', + }), + value: HeaderFooterTokenType.AGGREGATE_TOTAL, + }, + ]; + + topSection.sort((a, b) => a.label.localeCompare(b.label)); + bottomSection.sort((a, b) => a.label.localeCompare(b.label)); + + return [ + ...topSection, + { + label: '', + value: HeaderFooterTokenType.NEWLINE, + disabled: true, + }, + ...bottomSection, + ]; + }, [intl]); + return ( - + {(fieldProps) => ( {...fieldProps} required marginBottom0 - dataOptions={[ - // TODO: sort these alphabetically per i18n - { - label: 'Newline (LF)', - value: HeaderFooterTokenType.NEWLINE, - }, - { - label: 'Newline (Microsoft, CRLF)', - value: HeaderFooterTokenType.NEWLINE_MICROSOFT, - }, - { - label: 'Tab', - value: HeaderFooterTokenType.TAB, - }, - { - label: 'Comma', - value: HeaderFooterTokenType.COMMA, - }, - { - label: 'Whitespace', - value: HeaderFooterTokenType.SPACE, - }, - - { - label: '', - value: HeaderFooterTokenType.NEWLINE, - disabled: true, - }, - - { - label: 'Arbitrary text', - value: HeaderFooterTokenType.ARBITRARY_TEXT, - }, - { - label: 'Current date', - value: HeaderFooterTokenType.CURRENT_DATE, - }, - { - label: 'Number of accounts', - value: HeaderFooterTokenType.AGGREGATE_COUNT, - }, - { - label: 'Total amount', - value: HeaderFooterTokenType.AGGREGATE_TOTAL, - }, - ]} + dataOptions={options} /> )} diff --git a/src/components/Token/Shared/AmountWithDecimalToken.tsx b/src/components/Token/Shared/AmountWithDecimalToken.tsx index 6be3ec69..a475729c 100644 --- a/src/components/Token/Shared/AmountWithDecimalToken.tsx +++ b/src/components/Token/Shared/AmountWithDecimalToken.tsx @@ -1,8 +1,10 @@ import { Checkbox, Col } from '@folio/stripes/components'; import React from 'react'; import { Field } from 'react-final-form'; +import { useIntl } from 'react-intl'; export default function AmountWithDecimalToken({ prefix }: { prefix: string }) { + const intl = useIntl(); return ( @@ -10,14 +12,17 @@ export default function AmountWithDecimalToken({ prefix }: { prefix: string }) { )}

- If selected, amounts will be exported like “12.50”; if - left unselected, they will be exported like “1250”. + {intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.accountAmount.enableDecimal.description', + })}

diff --git a/src/components/Token/Shared/ArbitraryTextToken.tsx b/src/components/Token/Shared/ArbitraryTextToken.tsx index 240f5021..16c9c0a4 100644 --- a/src/components/Token/Shared/ArbitraryTextToken.tsx +++ b/src/components/Token/Shared/ArbitraryTextToken.tsx @@ -1,6 +1,7 @@ import { Col, TextField } from '@folio/stripes/components'; import React from 'react'; import { Field } from 'react-final-form'; +import { FormattedMessage } from 'react-intl'; export default function ArbitraryTextToken({ prefix }: { prefix: string }) { return ( @@ -12,7 +13,9 @@ export default function ArbitraryTextToken({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Value" + label={ + + } /> )}
diff --git a/src/components/Token/Shared/DatePartPicker.test.tsx b/src/components/Token/Shared/DatePartPicker.test.tsx index 610f8d87..be2e4e74 100644 --- a/src/components/Token/Shared/DatePartPicker.test.tsx +++ b/src/components/Token/Shared/DatePartPicker.test.tsx @@ -3,20 +3,23 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { Form } from 'react-final-form'; import DatePartPicker from './DatePartPicker'; +import withIntlConfiguration from '../../../test/util/withIntlConfiguration'; describe('Date part picker', () => { it('displays appropriate form', async () => { const submitter = jest.fn(); render( -
submitter(v)}> - {({ handleSubmit }) => ( - - - - - )} - + withIntlConfiguration( +
submitter(v)}> + {({ handleSubmit }) => ( + + + + + )} + + ) ); expect(screen.getByRole('combobox', { name: 'Format' })).toHaveDisplayValue( diff --git a/src/components/Token/Shared/DatePartPicker.tsx b/src/components/Token/Shared/DatePartPicker.tsx index d0dacdab..7aa072f5 100644 --- a/src/components/Token/Shared/DatePartPicker.tsx +++ b/src/components/Token/Shared/DatePartPicker.tsx @@ -2,8 +2,10 @@ import { Select } from '@folio/stripes/components'; import React from 'react'; import { Field } from 'react-final-form'; import { DateFormatType } from '../../../types/TokenTypes'; +import { FormattedMessage, useIntl } from 'react-intl'; export default function DatePartPicker({ prefix }: { prefix: string }) { + const intl = useIntl(); return ( {(fieldProps) => ( @@ -11,47 +13,69 @@ export default function DatePartPicker({ prefix }: { prefix: string }) { {...fieldProps} required marginBottom0 - label="Format" + label={ + + } dataOptions={[ { - label: 'Year (4-digit)', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.yearLong', + }), value: DateFormatType.YEAR_LONG, }, { - label: 'Year (2-digit)', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.yearShort', + }), value: DateFormatType.YEAR_SHORT, }, { - label: 'Month', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.month', + }), value: DateFormatType.MONTH, }, { - label: 'Day of month', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.date', + }), value: DateFormatType.DATE, }, { - label: 'Hour', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.hour', + }), value: DateFormatType.HOUR, }, { - label: 'Minute', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.minute', + }), value: DateFormatType.MINUTE, }, { - label: 'Second', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.second', + }), value: DateFormatType.SECOND, }, { - label: 'Quarter', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.quarter', + }), value: DateFormatType.QUARTER, }, { - label: 'ISO week number', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.isoWeekNum', + }), value: DateFormatType.WEEK_OF_YEAR_ISO, }, { - label: 'ISO week year', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.token.currentDate.format.isoWeekYear', + }), value: DateFormatType.WEEK_YEAR_ISO, }, ]} diff --git a/src/components/Token/Shared/TimezonePicker.tsx b/src/components/Token/Shared/TimezonePicker.tsx index 4d879721..239fa0e2 100644 --- a/src/components/Token/Shared/TimezonePicker.tsx +++ b/src/components/Token/Shared/TimezonePicker.tsx @@ -1,7 +1,7 @@ import { Select } from '@folio/stripes/components'; import React, { useMemo } from 'react'; import { Field } from 'react-final-form'; -import { useIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import timeZones from '../../../utils/timezones'; export default function TimezonePicker({ prefix }: { prefix: string }) { @@ -19,7 +19,9 @@ export default function TimezonePicker({ prefix }: { prefix: string }) { {...fieldProps} required marginBottom0 - label="Timezone" + label={ + + } dataOptions={timeZonesForSelect} /> )} diff --git a/src/components/Token/Shared/WhitespaceToken.tsx b/src/components/Token/Shared/WhitespaceToken.tsx index 0909b987..02f6a0fd 100644 --- a/src/components/Token/Shared/WhitespaceToken.tsx +++ b/src/components/Token/Shared/WhitespaceToken.tsx @@ -1,6 +1,7 @@ import { Col, TextField } from '@folio/stripes/components'; import React from 'react'; import { Field } from 'react-final-form'; +import { FormattedMessage } from 'react-intl'; export default function WhitespaceToken({ prefix }: { prefix: string }) { return ( @@ -14,7 +15,9 @@ export default function WhitespaceToken({ prefix }: { prefix: string }) { required type="number" min={1} - label="Number of spaces" + label={ + + } /> )} diff --git a/src/components/TransferAccountFields.tsx b/src/components/TransferAccountFields.tsx index 42dc3aad..a27589c2 100644 --- a/src/components/TransferAccountFields.tsx +++ b/src/components/TransferAccountFields.tsx @@ -3,6 +3,7 @@ import React, { useMemo } from 'react'; import { Field, useField } from 'react-final-form'; import useFeeFineOwners from '../api/queries/useFeeFineOwners'; import useTransferAccounts from '../api/queries/useTransferAccounts'; +import { FormattedMessage } from 'react-intl'; export default function TransferAccountFields({ prefix }: { prefix: string }) { const feeFineOwners = useFeeFineOwners(); @@ -48,11 +49,13 @@ export default function TransferAccountFields({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Fee/fine owner" + label={ + + } dataOptions={[ { label: '', value: undefined, disabled: true }, ...ownersSelectOptions, - ]} + ].sort((a, b) => a.label.localeCompare(b.label))} /> )}
@@ -65,11 +68,13 @@ export default function TransferAccountFields({ prefix }: { prefix: string }) { fullWidth marginBottom0 required - label="Transfer account" + label={ + + } dataOptions={[ { label: '', value: undefined }, ...accountSelectOptions, - ]} + ].sort((a, b) => a.label.localeCompare(b.label))} /> )} diff --git a/src/form/ConfigurationForm.test.tsx b/src/form/ConfigurationForm.test.tsx new file mode 100644 index 00000000..3cf9483f --- /dev/null +++ b/src/form/ConfigurationForm.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from '@testing-library/react'; +import arrayMutators from 'final-form-arrays'; +import React, { ComponentType } from 'react'; +import { Form, FormProps } from 'react-final-form'; +import withIntlConfiguration from '../test/util/withIntlConfiguration'; +import ConfigurationForm from './ConfigurationForm'; + +jest.mock('@folio/stripes/final-form', () => ({ + __esModule: true, + default: () => (Component: ComponentType) => (props: FormProps) => + ( +
+ {(formProps) => } + + ), +})); + +jest.mock('../api/queries/useFeeFineOwners', () => ({ + __esModule: true, + default: () => ({ data: [], isSuccess: true }), +})); + +jest.mock('../api/queries/useTransferAccounts', () => ({ + __esModule: true, + default: () => ({ data: [], isSuccess: true }), +})); + +describe('Configuration form', () => { + it('renders the configuration form', () => { + render( + withIntlConfiguration( + + ) + ); + + screen.debug(); + + expect(screen.getByText('Account data format')).toBeVisible(); + }); + + it('renders the configuration form with aggregate initial true', () => { + render( + withIntlConfiguration( + + ) + ); + + screen.debug(); + + expect(screen.getByText('Patron data format')).toBeVisible(); + }); +}); diff --git a/src/form/ConfigurationForm.tsx b/src/form/ConfigurationForm.tsx index f874cfa0..1d55f0a3 100644 --- a/src/form/ConfigurationForm.tsx +++ b/src/form/ConfigurationForm.tsx @@ -8,19 +8,8 @@ import { import stripesFinalForm from '@folio/stripes/final-form'; import { FormApi } from 'final-form'; import React, { FormEvent, MutableRefObject, useCallback } from 'react'; -import { FormRenderProps, FormSpy, useField } from 'react-final-form'; -import formValuesToDto from '../api/dto/to/formValuesToDto'; -import useCampuses from '../api/queries/useCampuses'; -import useCurrentConfig from '../api/queries/useCurrentConfig'; -import useFeeFineOwners from '../api/queries/useFeeFineOwners'; -import useFeeFineTypes from '../api/queries/useFeeFineTypes'; -import useInstitutions from '../api/queries/useInstitutions'; -import useLibraries from '../api/queries/useLibraries'; -import useLocations from '../api/queries/useLocations'; -import usePatronGroups from '../api/queries/usePatronGroups'; -import useServicePoints from '../api/queries/useServicePoints'; -import useTransferAccounts from '../api/queries/useTransferAccounts'; -import useInitialValues from '../hooks/useInitialValues'; +import { FormRenderProps, useField } from 'react-final-form'; +import { FormattedMessage } from 'react-intl'; import FormValues from '../types/FormValues'; import AggregateMenu from './sections/AggregateMenu'; import CriteriaMenu from './sections/CriteriaMenu'; @@ -65,82 +54,68 @@ function ConfigurationForm({ - + + } + > - + + } + > - + + } + > - + + } + > + ) : ( + + ) } > - + + } + > - + + } + > - + + } + > - - - - {({ values }) =>
{JSON.stringify(values, undefined, 2)}
} -
-
- - subscription={{ values: true }}> - {({ values }) => ( -
{JSON.stringify(formValuesToDto(values), undefined, 2)}
- )} - -
- -
{JSON.stringify(useCurrentConfig().data, undefined, 2)}
-
- -
{JSON.stringify(useInitialValues(), undefined, 2)}
-
- -
{JSON.stringify(usePatronGroups().data, undefined, 2)}
-
- -
{JSON.stringify(useServicePoints().data, undefined, 2)}
-
- -
{JSON.stringify(useFeeFineOwners().data, undefined, 2)}
-
- -
{JSON.stringify(useFeeFineTypes().data, undefined, 2)}
-
- -
{JSON.stringify(useTransferAccounts().data, undefined, 2)}
-
- -
{JSON.stringify(useInstitutions().data, undefined, 2)}
-
- -
{JSON.stringify(useCampuses().data, undefined, 2)}
-
- -
{JSON.stringify(useLibraries().data, undefined, 2)}
-
- -
{JSON.stringify(useLocations().data, undefined, 2)}
-
); @@ -148,5 +123,4 @@ function ConfigurationForm({ export default stripesFinalForm({ validateOnBlur: true, - // TODO: add validate, })(ConfigurationForm); diff --git a/src/form/sections/AggregateMenu.tsx b/src/form/sections/AggregateMenu.tsx index d26b57ee..23fb6413 100644 --- a/src/form/sections/AggregateMenu.tsx +++ b/src/form/sections/AggregateMenu.tsx @@ -2,6 +2,7 @@ import { Checkbox } from '@folio/stripes/components'; import React from 'react'; import { Field, useField } from 'react-final-form'; import AggregateCriteriaCard from '../../components/AggregateCriteria/AggregateCriteriaCard'; +import { FormattedMessage, useIntl } from 'react-intl'; export default function AggregateMenu() { const isAggregateEnabled = useField('aggregate', { @@ -9,17 +10,24 @@ export default function AggregateMenu() { format: (value) => value ?? false, }).input.value; + const intl = useIntl(); + return (
{(fieldProps) => ( - + )}

- If enabled, each output row will correspond to a single patron with - all of their accounts, rather than just a single account. +

diff --git a/src/form/sections/DataTokenMenu.tsx b/src/form/sections/DataTokenMenu.tsx index 9bf47077..559c6ec1 100644 --- a/src/form/sections/DataTokenMenu.tsx +++ b/src/form/sections/DataTokenMenu.tsx @@ -4,6 +4,7 @@ import { FieldArray } from 'react-final-form-arrays'; import DataTokenCard from '../../components/Token/Data/DataTokenCard'; import { DataTokenType } from '../../types/TokenTypes'; import { useField } from 'react-final-form'; +import { FormattedMessage } from 'react-intl'; export default function DataTokenMenu() { const aggregate = useField('aggregate', { @@ -32,7 +33,7 @@ export default function DataTokenMenu() { /> ))} )} diff --git a/src/form/sections/ExportPreview.test.tsx b/src/form/sections/ExportPreview.test.tsx index bba99337..de0a0268 100644 --- a/src/form/sections/ExportPreview.test.tsx +++ b/src/form/sections/ExportPreview.test.tsx @@ -5,6 +5,7 @@ import { Form } from 'react-final-form'; import ExportPreview from './ExportPreview'; import { DataTokenType, HeaderFooterTokenType } from '../../types/TokenTypes'; import userEvent from '@testing-library/user-event'; +import withIntlConfiguration from '../../test/util/withIntlConfiguration'; jest.mock('@ngneat/falso', () => ({ randFloat: jest.fn(() => 12.34), @@ -14,11 +15,12 @@ jest.mock('@ngneat/falso', () => ({ describe('Export preview component', () => { it('wraps lines when selected', async () => { const { container } = render( -
- {() => } - + withIntlConfiguration( +
+ {() => } + + ) ); - // when undefined expect(container.querySelector('.wrap')).toBeVisible(); diff --git a/src/form/sections/ExportPreview.tsx b/src/form/sections/ExportPreview.tsx index 8a0369e3..175d6820 100644 --- a/src/form/sections/ExportPreview.tsx +++ b/src/form/sections/ExportPreview.tsx @@ -4,6 +4,7 @@ import { Card, Checkbox } from '@folio/stripes/components'; import classNames from 'classnames'; import { Field, useField } from 'react-final-form'; import ExportPreviewData from '../../components/ExportPreview/ExportPreviewData'; +import { FormattedMessage, useIntl } from 'react-intl'; export default function ExportPreview() { const wrap = useField('preview.wrap', { @@ -11,23 +12,32 @@ export default function ExportPreview() { format: (value) => value ?? true, }).input.value; + const intl = useIntl(); + return ( <> + } bodyClass={classNames(css.preview, { [css.wrap]: wrap })} >

- This preview is only a sample and does not represent real data, nor - does it consider any specified criteria. +

{(fieldProps) => ( - + )} @@ -35,7 +45,9 @@ export default function ExportPreview() { )} diff --git a/src/form/sections/HeaderFooterMenu.tsx b/src/form/sections/HeaderFooterMenu.tsx index de99d6c7..122a85da 100644 --- a/src/form/sections/HeaderFooterMenu.tsx +++ b/src/form/sections/HeaderFooterMenu.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { FieldArray } from 'react-final-form-arrays'; import { HeaderFooterTokenType } from '../../types/TokenTypes'; import HeaderFooterCard from '../../components/Token/HeaderFooter/HeaderFooterCard'; +import { FormattedMessage } from 'react-intl'; export default function HeaderFooterMenu({ name }: { name: string }) { return ( @@ -21,7 +22,7 @@ export default function HeaderFooterMenu({ name }: { name: string }) { )} diff --git a/src/form/sections/SchedulingMenu.test.tsx b/src/form/sections/SchedulingMenu.test.tsx index 875fa68d..eb8e74ca 100644 --- a/src/form/sections/SchedulingMenu.test.tsx +++ b/src/form/sections/SchedulingMenu.test.tsx @@ -69,7 +69,7 @@ describe('Scheduling menu', () => { ).toBeVisible(); expect( screen.getByRole('textbox', { - name: (name) => name.startsWith('Run at'), + name: (name) => name.startsWith('Start time'), }) ).toBeVisible(); }); @@ -94,7 +94,7 @@ describe('Scheduling menu', () => { ).toBeVisible(); expect( screen.getByRole('textbox', { - name: (name) => name.startsWith('Run at'), + name: (name) => name.startsWith('Start time'), }) ).toBeVisible(); expect( diff --git a/src/form/sections/SchedulingMenu.tsx b/src/form/sections/SchedulingMenu.tsx index b110df59..2ed01002 100644 --- a/src/form/sections/SchedulingMenu.tsx +++ b/src/form/sections/SchedulingMenu.tsx @@ -9,18 +9,24 @@ import { } from '@folio/stripes/components'; import React from 'react'; import { Field, useField } from 'react-final-form'; -import { useIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { Weekday, useLocaleWeekdays } from '../../utils/WeekdayUtils'; import SchedulingFrequency from '../../types/SchedulingFrequency'; export function getIntervalLabel(frequency: SchedulingFrequency) { switch (frequency) { case SchedulingFrequency.Hours: - return 'Hours between runs'; + return ( + + ); case SchedulingFrequency.Days: - return 'Days between runs'; + return ( + + ); case SchedulingFrequency.Weeks: - return 'Weeks between runs'; + return ( + + ); default: return ''; } @@ -31,7 +37,8 @@ export default function SchedulingMenu() { subscription: { value: true }, }).input.value; - const localeWeekdays = useLocaleWeekdays(useIntl()); + const intl = useIntl(); + const localeWeekdays = useLocaleWeekdays(intl); return ( @@ -45,22 +52,32 @@ export default function SchedulingMenu() { {...fieldProps} fullWidth required - label="Frequency" + label={ + + } dataOptions={[ { - label: 'Never (run manually)', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.scheduling.frequency.manual', + }), value: SchedulingFrequency.Manual, }, { - label: 'Hours', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.scheduling.frequency.hours', + }), value: SchedulingFrequency.Hours, }, { - label: 'Days', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.scheduling.frequency.days', + }), value: SchedulingFrequency.Days, }, { - label: 'Weeks', + label: intl.formatMessage({ + id: 'ui-plugin-bursar-export.bursarExports.scheduling.frequency.weeks', + }), value: SchedulingFrequency.Weeks, }, ]} @@ -93,19 +110,27 @@ export default function SchedulingMenu() { {(fieldProps) => ( - + )} )} {frequencyValue === SchedulingFrequency.Weeks && ( - + {(fieldProps) => ( > {...fieldProps} required - label="Run on weekdays" + label={ + + } dataOptions={localeWeekdays.map((weekday) => ({ label: weekday.long, value: weekday.weekday, diff --git a/src/form/sections/TransferInfoMenu.tsx b/src/form/sections/TransferInfoMenu.tsx index e383cea5..4f40acaf 100644 --- a/src/form/sections/TransferInfoMenu.tsx +++ b/src/form/sections/TransferInfoMenu.tsx @@ -4,6 +4,7 @@ import { FieldArray } from 'react-final-form-arrays'; import ConditionalCard from '../../components/ConditionalCard'; import TransferAccountFields from '../../components/TransferAccountFields'; import { CriteriaTerminalType } from '../../types/CriteriaTypes'; +import { FormattedMessage } from 'react-intl'; export default function TransferInfoMenu() { return ( @@ -21,7 +22,15 @@ export default function TransferInfoMenu() { ))} - + + ) : ( + + ) + } + > @@ -32,14 +41,12 @@ export default function TransferInfoMenu() { }) } > - Add condition +

- Conditions will be evaluated in order, with the first matched - transfer account being used. If no conditions are matched, the - account listed under “otherwise” will be used. +

diff --git a/translations/ui-plugin-bursar-export/en.json b/translations/ui-plugin-bursar-export/en.json index 5d26a3f6..62e812b8 100644 --- a/translations/ui-plugin-bursar-export/en.json +++ b/translations/ui-plugin-bursar-export/en.json @@ -1,40 +1,154 @@ { - "meta.title": "Transfer criteria", - - "bursarExports": "Bursar exports", - "bursarExports.schedulePeriod": "Schedule period", - "bursarExports.schedulePeriod.none": "None", - "bursarExports.schedulePeriod.hours": "Hours", - "bursarExports.schedulePeriod.days": "Days", - "bursarExports.schedulePeriod.weeks": "Weeks", - "bursarExports.scheduleFrequency": "Schedule frequency", - "bursarExports.scheduleWeekdays": "Weekdays", - "bursarExports.scheduleWeekdays.friday": "F", - "bursarExports.scheduleWeekdays.monday": "M", - "bursarExports.scheduleWeekdays.saturday": "S", - "bursarExports.scheduleWeekdays.sunday": "S", - "bursarExports.scheduleWeekdays.thursday": "T", - "bursarExports.scheduleWeekdays.tuesday": "T", - "bursarExports.scheduleWeekdays.wednesday": "W", - "bursarExports.scheduleTime": "Schedule time", - "bursarExports.daysOutstanding": "Fees/Fines older than (days)", - "bursarExports.patronGroups": "Patron groups", - "bursarExports.transferOwner": "Transfer owner", - "bursarExports.transferAccount": "Transfer account", - "bursarExports.owner": "Fee/fine owner", - "bursarExports.itemTypes": "Transfer types", - "bursarExports.feeFineType": "Fee/fine type", - "bursarExports.itemType": "Transfer type", - "bursarExports.itemDescription": "Transfer description", - "bursarExports.itemCode": "Transfer code", - "bursarExports.itemCode.CHARGE": "Charge", - "bursarExports.itemCode.PAYMENT": "Payment", - "bursarExports.save": "Save", - "bursarExports.save.success": "Bursar exports configuration has been successfully saved", - "bursarExports.save.error": "Bursar exports configuration was not saved", - "bursarExports.runManually": "Run manually", - "bursarExports.runManually.success": "Bursar exports has been successfully scheduled", - "bursarExports.runManually.error": "Bursar exports was not scheduled", + "meta.title": "Transfer configuration", + + "bursarExports.paneTitle": "Transfer configuration", + + "bursarExports.button.addCondition": "Add condition", + "bursarExports.button.add": "Add", + "bursarExports.button.save": "Save", + "bursarExports.button.runManually": "Run manually", + "bursarExports.otherwise": "Otherwise:", + + "bursarExports.scheduling.accordion": "Scheduling", + "bursarExports.scheduling.frequency": "Frequency", + "bursarExports.scheduling.frequency.manual": "Never (run manually)", + "bursarExports.scheduling.frequency.hours": "Hours", + "bursarExports.scheduling.frequency.days": "Days", + "bursarExports.scheduling.frequency.weeks": "Weeks", + "bursarExports.scheduling.interval.hours": "Hours between runs", + "bursarExports.scheduling.interval.days": "Days between runs", + "bursarExports.scheduling.interval.weeks": "Weeks between runs", + "bursarExports.scheduling.time": "Start time", + "bursarExports.scheduling.weekdays": "Run on weekdays", + + "bursarExports.criteria.accordion": "Criteria", + "bursarExports.criteria.select.allOf": "All of:", + "bursarExports.criteria.select.anyOf": "Any of:", + "bursarExports.criteria.select.noneOf": "None of:", + "bursarExports.criteria.select.age": "Age", + "bursarExports.criteria.select.amount": "Amount", + "bursarExports.criteria.select.owner": "Fee/fine owner", + "bursarExports.criteria.select.type": "Fee/fine type", + "bursarExports.criteria.select.location": "Item location", + "bursarExports.criteria.select.servicePoint": "Item service point", + "bursarExports.criteria.select.patronGroup": "Patron group", + "bursarExports.criteria.select.none": "No criteria (always run)", + "bursarExports.criteria.select.label": "Criteria", + + "bursarExports.criteria.age.value": "Older than (days)", + "bursarExports.criteria.type.automatic": "Automatic", + "bursarExports.criteria.location.inst": "Institution", + "bursarExports.criteria.location.camp": "Campus", + "bursarExports.criteria.location.lib": "Library", + "bursarExports.criteria.location.loc": "Location", + "bursarExports.criteria.servicePoint.value": "Service point", + + "bursarExports.aggregate.accordion": "Aggregate by patron", + "bursarExports.aggregate.enabler": "Group data by patron", + "bursarExports.aggregate.description": "If enabled, each output row will correspond to a single patron with all of their accounts, rather than just a single account.", + "bursarExports.aggregate.filter": "Filter type", + "bursarExports.aggregate.filter.header": "Only include patrons with:", + "bursarExports.aggregate.filter.none": "None (include all patrons)", + "bursarExports.aggregate.filter.numAccounts": "Number of accounts", + "bursarExports.aggregate.filter.numAccounts.amount": "Number of accounts", + "bursarExports.aggregate.filter.totalAmount": "Total amount", + "bursarExports.aggregate.filter.totalAmount.amount": "Amount", + "bursarExports.aggregate.filter.operator": "Comparison operator", + "bursarExports.aggregate.filter.operator.less": "Less than but not equal to", + "bursarExports.aggregate.filter.operator.lessEqual": "Less than or equal to", + "bursarExports.aggregate.filter.operator.greater": "Greater than but not equal to", + "bursarExports.aggregate.filter.operator.greaterEqual": "Greater than or equal to", + "bursarExports.aggregate.filter.description": "This will be applied after accounts are evaluated per the \u201cCriteria\u201d specified above.", + + "bursarExports.header.accordion": "Header format", + "bursarExports.footer.accordion": "Footer format", + + "bursarExports.token.newline": "Newline (LF)", + "bursarExports.token.newlineMicrosoft": "Newline (Microsoft, CRLF)", + "bursarExports.token.tab": "Tab", + "bursarExports.token.comma": "Comma", + "bursarExports.token.whitespace": "Whitespace", + "bursarExports.token.arbitraryText": "Arbitrary text", + "bursarExports.token.currentDate": "Current date", + "bursarExports.token.constantConditional": "Conditional text", + "bursarExports.token.numAccounts": "Number of accounts", + "bursarExports.token.totalAmount": "Total amount", + "bursarExports.token.userData": "User info", + "bursarExports.token.accountAmount": "Account amount", + "bursarExports.token.accountDate": "Account date", + "bursarExports.token.feeFineType": "Fee/fine type", + "bursarExports.token.itemInfo": "Item info", + "bursarExports.token.fallback": "Fallback value", + "bursarExports.token.fallback.description": "If the chosen date is not available/applicable, the fallback value will be used instead.", + "bursarExports.token.value": "Value", + + "bursarExports.token.headerFooter.typeSelect": "Header/footer type select", + + "bursarExports.token.dataType.typeSelect": "Data type select", + + "bursarExports.token.whitespace.numSpaces": "Number of spaces", + + "bursarExports.token.currentDate.format": "Format", + "bursarExports.token.currentDate.format.yearLong": "Year (4-digit)", + "bursarExports.token.currentDate.format.yearShort": "Year (2-digit)", + "bursarExports.token.currentDate.format.month": "Month", + "bursarExports.token.currentDate.format.date": "Day of month", + "bursarExports.token.currentDate.format.hour": "Hour", + "bursarExports.token.currentDate.format.minute": "Minute", + "bursarExports.token.currentDate.format.second": "Second", + "bursarExports.token.currentDate.format.quarter": "Quarter", + "bursarExports.token.currentDate.format.isoWeekNum": "ISO week number", + "bursarExports.token.currentDate.format.isoWeekYear": "ISO week year", + "bursarExports.token.currentDate.timezone": "Timezone", + + "bursarExports.token.accountAmount.enableDecimal": "Include the decimal point", + "bursarExports.token.accountAmount.enableDecimal.description": "If selected, amounts will be exported like \u201c12.50\u201d if left unselected, they will be exported like \u201c1250\u201d.", + + "bursarExports.token.accountDate.dateType": "Date", + "bursarExports.token.accountDate.dateType.created": "Creation date", + "bursarExports.token.accountDate.dateType.updated": "Last updated date", + "bursarExports.token.accountDate.dateType.dueItem": "Item due date", + "bursarExports.token.accountDate.dateType.dueLoan": "Loan end date", + "bursarExports.token.accountDate.fallback.description": "If the chosen date is not available/applicable, the fallback value will be used instead.", + + "bursarExports.token.feeFineType.attribute": "Attribute", + "bursarExports.token.feeFineType.name": "Type name", + "bursarExports.token.feeFineType.id": "Type ID", + + "bursarExports.token.itemInfo.name": "Name", + "bursarExports.token.itemInfo.barcode": "Barcode", + "bursarExports.token.itemInfo.material": "Material type", + "bursarExports.token.itemInfo.instId": "Institution ID", + "bursarExports.token.itemInfo.campId": "Campus ID", + "bursarExports.token.itemInfo.libId": "Library ID", + "bursarExports.token.itemInfo.locId": "Location ID", + + "bursarExports.token.userInfo.folioId": "Folio ID", + "bursarExports.token.userInfo.extId": "External ID", + "bursarExports.token.userInfo.groupId": "Patron group ID", + "bursarExports.token.userInfo.barcode": "Barcode", + "bursarExports.token.userInfo.username": "Username", + "bursarExports.token.userInfo.firstname": "First name", + "bursarExports.token.userInfo.middlename": "Middle name", + "bursarExports.token.userInfo.lastname": "Last name", + + "bursarExports.token.constantConditional.value": "Then use:", + "bursarExports.token.constantConditional.description": "Conditions will be evaluated in order, with the first matched value being used. If no conditions are matched, the fallback value will be used.", + + "bursarExports.data.accordion.patron": "Patron data format", + "bursarExports.data.accordion.account": "Account data format", + + "bursarExports.preview.accordion": "Preview", + "bursarExports.preview.header": "Export preview", + "bursarExports.preview.wrap": "Wrap long lines", + "bursarExports.preview.description": "This preview is only a sample and does not represent real data, nor does it consider any specified criteria.", + "bursarExports.preview.enableInvisibleChar": "Display invisible characters (newlines, tabs, and spaces)", + + "bursarExports.transfer.accordion": "Transfer accounts to", + "bursarExports.transfer.description": "Conditions will be evaluated in order, with the first matched transfer account being used. If no conditions are matched, the account listed under \u201cotherwise\u201d will be used.", + "bursarExports.transfer.owner": "Fee/fine owner", + "bursarExports.transfer.account": "Transfer account", + "bursarExports.transfer.transferTo": "Transfer to:", "permission.bursar-exports.all": "Transfer exports: Modify configuration and start jobs", "permission.bursar-exports.manual": "Transfer exports: Start manual jobs",