Skip to content

Commit ff63ccb

Browse files
committed
Introduce minimal test ui
1 parent 51b93be commit ff63ccb

18 files changed

+1290
-4
lines changed

packages/ra-core/src/controller/list/WithListContext.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactElement } from 'react';
1+
import React, { ReactNode } from 'react';
22
import { RaRecord } from '../../types';
33
import { ListControllerResult } from './useListController';
44
import { useListContextWithProps } from './useListContextWithProps';
@@ -79,9 +79,7 @@ export interface WithListContextProps<RecordType extends RaRecord>
7979
>
8080
>
8181
> {
82-
render?: (
83-
context: Partial<ListControllerResult<RecordType>>
84-
) => ReactElement | false | null;
82+
render?: (context: Partial<ListControllerResult<RecordType>>) => ReactNode;
8583
loading?: React.ReactNode;
8684
offline?: React.ReactNode;
8785
errorState?: ListControllerResult<RecordType>['error'];

packages/ra-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './routing';
1313
export * from './store';
1414
export * from './types';
1515
export * from './util';
16+
export * from './test-ui';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as React from 'react';
2+
import { CoreAdmin, CoreAdminProps } from '../core';
3+
4+
import { Layout } from './Layout';
5+
import { defaultI18nProvider } from './defaultI18nProvider';
6+
7+
export const Admin = (props: CoreAdminProps) => {
8+
const { layout = Layout } = props;
9+
return (
10+
<CoreAdmin
11+
i18nProvider={defaultI18nProvider}
12+
layout={layout}
13+
{...props}
14+
/>
15+
);
16+
};
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {
2+
ArrayInputContext,
3+
FieldTitle,
4+
InputProps,
5+
OptionalResourceContextProvider,
6+
SourceContextProvider,
7+
type SourceContextValue,
8+
composeSyncValidators,
9+
isRequired,
10+
useApplyInputDefaultValues,
11+
useFormGroupContext,
12+
useFormGroups,
13+
useGetValidationErrorMessage,
14+
useSourceContext,
15+
} from '../';
16+
import * as React from 'react';
17+
import { useEffect } from 'react';
18+
import { useFieldArray, useFormContext } from 'react-hook-form';
19+
20+
export const ArrayInput = (props: ArrayInputProps) => {
21+
const {
22+
defaultValue = [],
23+
label,
24+
isPending,
25+
children,
26+
resource: resourceFromProps,
27+
source: arraySource,
28+
validate,
29+
} = props;
30+
31+
const formGroupName = useFormGroupContext();
32+
const formGroups = useFormGroups();
33+
const parentSourceContext = useSourceContext();
34+
const finalSource = parentSourceContext.getSource(arraySource);
35+
36+
const sanitizedValidate = Array.isArray(validate)
37+
? composeSyncValidators(validate)
38+
: validate;
39+
const getValidationErrorMessage = useGetValidationErrorMessage();
40+
41+
const { getValues } = useFormContext();
42+
43+
const fieldProps = useFieldArray({
44+
name: finalSource,
45+
rules: {
46+
validate: async value => {
47+
if (!sanitizedValidate) return true;
48+
const error = await sanitizedValidate(
49+
value,
50+
getValues(),
51+
props
52+
);
53+
54+
if (!error) return true;
55+
return getValidationErrorMessage(error);
56+
},
57+
},
58+
});
59+
60+
useEffect(() => {
61+
if (formGroups && formGroupName != null) {
62+
formGroups.registerField(finalSource, formGroupName);
63+
}
64+
65+
return () => {
66+
if (formGroups && formGroupName != null) {
67+
formGroups.unregisterField(finalSource, formGroupName);
68+
}
69+
};
70+
}, [finalSource, formGroups, formGroupName]);
71+
72+
useApplyInputDefaultValues({
73+
inputProps: { ...props, defaultValue },
74+
isArrayInput: true,
75+
fieldArrayInputControl: fieldProps,
76+
});
77+
78+
// The SourceContext will be read by children of ArrayInput to compute their composed source and label
79+
//
80+
// <ArrayInput source="orders" /> => SourceContext is "orders"
81+
// <SimpleFormIterator> => SourceContext is "orders.0"
82+
// <DateInput source="date" /> => final source for this input will be "orders.0.date"
83+
// </SimpleFormIterator>
84+
// </ArrayInput>
85+
//
86+
const sourceContext = React.useMemo<SourceContextValue>(
87+
() => ({
88+
// source is the source of the ArrayInput child
89+
getSource: (source: string) => {
90+
if (!source) {
91+
// SimpleFormIterator calls getSource('') to get the arraySource
92+
return parentSourceContext.getSource(arraySource);
93+
}
94+
95+
// We want to support nesting and composition with other inputs (e.g. TranslatableInputs, ReferenceOneInput, etc),
96+
// we must also take into account the parent SourceContext
97+
//
98+
// <ArrayInput source="orders" /> => SourceContext is "orders"
99+
// <SimpleFormIterator> => SourceContext is "orders.0"
100+
// <DateInput source="date" /> => final source for this input will be "orders.0.date"
101+
// <ArrayInput source="items" /> => SourceContext is "orders.0.items"
102+
// <SimpleFormIterator> => SourceContext is "orders.0.items.0"
103+
// <TextInput source="reference" /> => final source for this input will be "orders.0.items.0.reference"
104+
// </SimpleFormIterator>
105+
// </ArrayInput>
106+
// </SimpleFormIterator>
107+
// </ArrayInput>
108+
return parentSourceContext.getSource(
109+
`${arraySource}.${source}`
110+
);
111+
},
112+
// if Array source is items, and child source is name, .0.name => resources.orders.fields.items.name
113+
getLabel: (source: string) =>
114+
parentSourceContext.getLabel(`${arraySource}.${source}`),
115+
}),
116+
[parentSourceContext, arraySource]
117+
);
118+
119+
if (isPending) {
120+
return null;
121+
}
122+
123+
return (
124+
<div>
125+
<div>
126+
<FieldTitle
127+
label={label}
128+
source={arraySource}
129+
resource={resourceFromProps}
130+
isRequired={isRequired(validate)}
131+
/>
132+
</div>
133+
<ArrayInputContext.Provider value={fieldProps}>
134+
<OptionalResourceContextProvider value={resourceFromProps}>
135+
<SourceContextProvider value={sourceContext}>
136+
{children}
137+
</SourceContextProvider>
138+
</OptionalResourceContextProvider>
139+
</ArrayInputContext.Provider>
140+
</div>
141+
);
142+
};
143+
144+
export interface ArrayInputProps
145+
extends Omit<InputProps, 'disabled' | 'readOnly'> {
146+
className?: string;
147+
children: React.ReactNode;
148+
isFetching?: boolean;
149+
isLoading?: boolean;
150+
isPending?: boolean;
151+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as React from 'react';
2+
import {
3+
FieldTitle,
4+
InputProps,
5+
isRequired,
6+
useChoicesContext,
7+
useInput,
8+
} from '../';
9+
10+
export const AutocompleteArrayInput = (props: Partial<InputProps>) => {
11+
const { allChoices, source, setFilters, filterValues } =
12+
useChoicesContext();
13+
14+
const { field, fieldState } = useInput({ source, ...props });
15+
16+
return (
17+
<div>
18+
<div>
19+
<FieldTitle
20+
label={props.label}
21+
source={props.source}
22+
resource={props.resource}
23+
isRequired={isRequired(props.validate)}
24+
/>
25+
</div>
26+
<input
27+
type="text"
28+
value={filterValues['q']}
29+
onChange={e =>
30+
setFilters({ ...filterValues, q: e.target.value })
31+
}
32+
/>
33+
<button type="button" onClick={() => field.onChange([])}>
34+
Clear value
35+
</button>
36+
<ul>
37+
{allChoices?.map(choice => (
38+
<li key={choice.id}>
39+
<label>
40+
<input
41+
type="checkbox"
42+
value={choice.id}
43+
onChange={event => {
44+
const newValue = event.target.checked
45+
? [...field.value, choice.id]
46+
: field.value.filter(
47+
(v: any) => v !== choice.id
48+
);
49+
field.onChange(newValue);
50+
}}
51+
checked={field.value.includes(choice.id)}
52+
aria-describedby={
53+
fieldState.error
54+
? `error-${props.source}`
55+
: undefined
56+
}
57+
/>
58+
{choice.name}
59+
</label>
60+
</li>
61+
))}
62+
</ul>
63+
{fieldState.error ? (
64+
<p id={`error-${props.source}`} style={{ color: 'red' }}>
65+
{fieldState.error.message}
66+
</p>
67+
) : null}
68+
</div>
69+
);
70+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Translate } from '../';
2+
import * as React from 'react';
3+
4+
export const Confirm = ({
5+
isOpen,
6+
content,
7+
onClose,
8+
onConfirm,
9+
title,
10+
translateOptions = {},
11+
titleTranslateOptions = translateOptions,
12+
contentTranslateOptions = translateOptions,
13+
}: {
14+
isOpen: boolean;
15+
title: string;
16+
content: string;
17+
onConfirm: () => void;
18+
onClose: () => void;
19+
translateOptions?: Record<string, any>;
20+
titleTranslateOptions?: Record<string, any>;
21+
contentTranslateOptions?: Record<string, any>;
22+
}) => {
23+
return isOpen ? (
24+
<div
25+
style={{
26+
position: 'fixed',
27+
top: 0,
28+
left: 0,
29+
right: 0,
30+
bottom: 0,
31+
display: 'flex',
32+
alignItems: 'center',
33+
justifyContent: 'center',
34+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
35+
}}
36+
>
37+
<div
38+
style={{
39+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
40+
color: 'white',
41+
padding: '1em',
42+
}}
43+
>
44+
<p>
45+
{typeof title === 'string' ? (
46+
<Translate
47+
i18nKey={title}
48+
options={{
49+
_: title,
50+
...titleTranslateOptions,
51+
}}
52+
/>
53+
) : (
54+
title
55+
)}
56+
</p>
57+
<p>
58+
{typeof content === 'string' ? (
59+
<Translate
60+
i18nKey={content}
61+
options={{
62+
_: content,
63+
...contentTranslateOptions,
64+
}}
65+
/>
66+
) : (
67+
content
68+
)}
69+
</p>
70+
<div style={{ display: 'flex', gap: '1em' }}>
71+
<button onClick={onConfirm} type="button">
72+
<Translate i18nKey="ra.action.confirm">
73+
Confirm
74+
</Translate>
75+
</button>
76+
<button onClick={onClose} type="button">
77+
<Translate i18nKey="ra.action.cancel">Cancel</Translate>
78+
</button>
79+
</div>
80+
</div>
81+
</div>
82+
) : null;
83+
};

0 commit comments

Comments
 (0)