Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TS support to fields #8863

Merged
merged 35 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b7635e3
Add TS support to simple fields
djhi Apr 27, 2023
7dfa476
Fix demo types
djhi Apr 27, 2023
51d9352
Update all fields to support source inference
djhi May 3, 2023
0be2db9
Fix localforage and localstorage dataProviders
djhi May 3, 2023
e1cdc22
Add documentation
djhi May 3, 2023
973d573
Add BooleanField doc
djhi May 3, 2023
43f8e72
Revert to using RaRecord where possible
djhi May 11, 2023
ffc9e0d
Revert unnecessary changes
djhi May 11, 2023
94d6445
Revert more unnecessary changes
djhi May 11, 2023
2badd8a
Revert more unnecessary changes
djhi May 11, 2023
e66321d
Make it backward compatible
djhi May 13, 2023
7ca9511
Revert unnecessary changes
djhi May 13, 2023
356fed5
Revert more unnecessary changes
djhi May 13, 2023
63557cc
Add info about RaRecord
djhi May 13, 2023
e3b2a3f
Revert more unnecessary changes
djhi May 13, 2023
dcbc0e2
Apply suggestions from code review
djhi May 15, 2023
d54ea87
Revert CRM types changes
djhi May 15, 2023
cd69c5a
Add invalid TS example in Fields documentation
djhi May 15, 2023
8cefb44
Fix defaultProps breaking change
djhi May 15, 2023
91d22ac
Revert unnecessary change
djhi May 15, 2023
114bb31
Revert demo changes
djhi May 15, 2023
b0af5cd
Revert SortPayload changes
djhi May 15, 2023
7f32840
Revert SaveButton changes
djhi May 15, 2023
022d33d
Make BooleanField stories use a Resource
djhi May 15, 2023
b8b084f
Use RaRecord In ReferenceArrayField for now
djhi May 15, 2023
448a49d
Reorganization
djhi May 15, 2023
ed9128f
Fix string casting
djhi May 15, 2023
69e46f8
Add support for tsx in prism
djhi May 15, 2023
6463e7a
Fix FileFIeld and ImageField
djhi May 15, 2023
8b5f394
Revert withLifecycleCallbacks changes
djhi May 15, 2023
5ccd5ed
SImplify defaultProps handling
djhi May 15, 2023
aa2ae67
Backport ReferenceFieldView fix
djhi May 15, 2023
1776372
Fix tests
djhi May 15, 2023
3ef347e
Move hotscript in dev deps
djhi May 16, 2023
b914e0e
Revert ArrayField proptypes
djhi May 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion docs/Fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,4 +597,39 @@ You can find components for react-admin in third-party repositories.

- [OoDeLally/react-admin-clipboard-list-field](https://github.com/OoDeLally/react-admin-clipboard-list-field): a quick and customizable copy-to-clipboard field.
- [MrHertal/react-admin-json-view](https://github.com/MrHertal/react-admin-json-view): JSON field and input for react-admin.
- [alexgschwend/react-admin-color-picker](https://github.com/alexgschwend/react-admin-color-picker): a color field
- [alexgschwend/react-admin-color-picker](https://github.com/alexgschwend/react-admin-color-picker): a color field

## TypeScript

All field components accept a generic type that describes the record. This lets TypeScript validate that the `source` prop targets an actual field of the record:

```tsx
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
import * as React from "react";
import { Show, SimpleShowLayout, TextField, DateField, RichTextField } from 'react-admin';

// Note that you shouldn't extend RaRecord for this to work
type Post = {
id: number;
title: string;
teaser: string;
body: string;
published_at: string;
}

export const PostShow = () => (
<Show>
<SimpleShowLayout>
<TextField<Post> source="title" />
<TextField<Post> source="teaser" />
{/* Here TS will show an error because a teasr field does not exist */}
<TextField<Post> source="teasr" />
<RichTextField<Post> source="body" />
<DateField<Post> label="Publication date" source="published_at" />
</SimpleShowLayout>
djhi marked this conversation as resolved.
Show resolved Hide resolved
</Show>
);
```

**Limitation**: You must not extend `RaRecord` for this to work. This is because `RaRecord` extends `Record<string, any>` and TypeScript would not be able to infer your types properties.

Specifying the record type will also allow your IDE to provide auto-completion for both the `source` and `sortBy` prop. Note that the `sortBy` prop also accepts any string.
20 changes: 11 additions & 9 deletions docs/js/prism.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions examples/crm/src/companies/LogoField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { useRecordContext } from 'react-admin';
import { Box } from '@mui/material';

import { Company, Contact } from '../types';
import { Company } from '../types';

const sizeInPixel = {
medium: 42,
Expand All @@ -14,7 +14,7 @@ export const LogoField = ({
}: {
size?: 'small' | 'medium';
}) => {
const record = useRecordContext<Company | Contact>();
const record = useRecordContext<Company>();
if (!record) return null;
return (
<Box
Expand Down
6 changes: 2 additions & 4 deletions examples/crm/src/contacts/TagsListEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ export const TagsListEdit = () => {
};

const handleDeleteTag = (id: Identifier) => {
const tags: Identifier[] = record.tags.filter(
(tagId: Identifier) => tagId !== id
);
const tags = record.tags.filter(tagId => tagId !== id);
update('contacts', {
id: record.id,
data: { tags },
Expand All @@ -72,7 +70,7 @@ export const TagsListEdit = () => {
};

const handleAddTag = (id: Identifier) => {
const tags: Identifier[] = [...record.tags, id];
const tags = [...record.tags, id];
update('contacts', {
id: record.id,
data: { tags },
Expand Down
4 changes: 2 additions & 2 deletions examples/crm/src/dataGenerator/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { RaRecord } from 'react-admin';
import { Company, Contact, ContactNote, Deal, Tag } from '../types';
import { Company, Contact, ContactNote, Deal, Sale, Tag } from '../types';

export interface Db {
companies: Company[];
contacts: Contact[];
contactNotes: ContactNote[];
deals: Deal[];
dealNotes: RaRecord[];
sales: RaRecord[];
sales: Sale[];
tags: Tag[];
tasks: RaRecord[];
}
5 changes: 4 additions & 1 deletion examples/crm/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RaRecord, Identifier } from 'react-admin';
import { Identifier, RaRecord } from 'react-admin';

export interface Sale extends RaRecord {
first_name: string;
Expand Down Expand Up @@ -38,6 +38,8 @@ export interface Contact extends RaRecord {
gender: string;
sales_id: Identifier;
nb_notes: number;
status: string;
background: string;
}

export interface ContactNote extends RaRecord {
Expand All @@ -59,6 +61,7 @@ export interface Deal extends RaRecord {
amount: number;
created_at: string;
updated_at: string;
start_at: string;
sales_id: Identifier;
index: number;
nb_notes: number;
Expand Down
2 changes: 1 addition & 1 deletion examples/demo/src/products/ProductReferenceField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface Props {

const ProductReferenceField = (
props: Props &
Omit<Omit<ReferenceFieldProps, 'source'>, 'reference' | 'children'>
Omit<ReferenceFieldProps, 'source' | 'reference' | 'children'>
) => (
<ReferenceField
label="Product"
Expand Down
4 changes: 2 additions & 2 deletions examples/demo/src/reviews/ReviewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import {
} from 'react-admin';

import AvatarField from '../visitors/AvatarField';
import { Customer } from './../types';
import { Customer, Review } from './../types';

export const ReviewItem = () => {
const record = useRecordContext();
const record = useRecordContext<Review>();
const createPath = useCreatePath();
if (!record) {
return null;
Expand Down
18 changes: 14 additions & 4 deletions examples/demo/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RaRecord, Identifier } from 'react-admin';
import { Identifier, RaRecord } from 'react-admin';

export type ThemeName = 'light' | 'dark';

Expand Down Expand Up @@ -35,6 +35,7 @@ export interface Customer extends RaRecord {
groups: string[];
nb_commands: number;
total_spent: number;
email: string;
}

export type OrderStatus = 'ordered' | 'delivered' | 'cancelled';
Expand All @@ -44,14 +45,22 @@ export interface Order extends RaRecord {
basket: BasketItem[];
date: Date;
total: number;
total_ex_taxes: number;
delivery_fees: number;
tax_rate: number;
taxes: number;
customer_id: Identifier;
reference: string;
}

export interface BasketItem {
export type BasketItem = {
product_id: Identifier;
quantity: number;
}
};

export interface Invoice extends RaRecord {}
export interface Invoice extends RaRecord {
date: Date;
}

export type ReviewStatus = 'accepted' | 'pending' | 'rejected';

Expand All @@ -60,6 +69,7 @@ export interface Review extends RaRecord {
status: ReviewStatus;
customer_id: Identifier;
product_id: Identifier;
comment: string;
}

declare global {
Expand Down
2 changes: 1 addition & 1 deletion examples/demo/src/visitors/FullNameField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const FullNameField = (props: Props) => {
};

FullNameField.defaultProps = {
source: 'last_name',
source: 'last_name' as const,
label: 'resources.customers.fields.name',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { useGetManyAggregate } from '../../dataProvider';
import { ListControllerResult, useList } from '../list';
import { useNotify } from '../../notification';

export interface UseReferenceArrayFieldControllerParams {
export interface UseReferenceArrayFieldControllerParams<
RecordType extends RaRecord = RaRecord
> {
filter?: any;
page?: number;
perPage?: number;
record?: RaRecord;
record?: RecordType;
reference: string;
resource: string;
sort?: SortPayload;
Expand Down Expand Up @@ -43,8 +45,11 @@ const defaultSort = { field: null, order: null };
*
* @returns {ListControllerResult} The reference props
*/
export const useReferenceArrayFieldController = (
props: UseReferenceArrayFieldControllerParams
export const useReferenceArrayFieldController = <
RecordType extends RaRecord = RaRecord,
ReferenceRecordType extends RaRecord = RaRecord
>(
props: UseReferenceArrayFieldControllerParams<RecordType>
): ListControllerResult => {
const {
filter = defaultFilter,
Expand All @@ -64,7 +69,9 @@ export const useReferenceArrayFieldController = (
return emptyArray;
}, [value, source]);

const { data, error, isLoading, isFetching, refetch } = useGetManyAggregate(
const { data, error, isLoading, isFetching, refetch } = useGetManyAggregate<
ReferenceRecordType
>(
reference,
{ ids },
{
Expand All @@ -88,7 +95,7 @@ export const useReferenceArrayFieldController = (
}
);

const listProps = useList({
const listProps = useList<ReferenceRecordType>({
data,
error,
filter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import isEqual from 'lodash/isEqual';
import { useSafeSetState, removeEmpty } from '../../util';
import { useGetManyReference } from '../../dataProvider';
import { useNotify } from '../../notification';
import { RaRecord, SortPayload } from '../../types';
import { Identifier, RaRecord, SortPayload } from '../../types';
import { ListControllerResult } from '../list';
import usePaginationState from '../usePaginationState';
import { useRecordSelection } from '../list/useRecordSelection';
import useSortState from '../useSortState';
import { useResourceContext } from '../../core';

export interface UseReferenceManyFieldControllerParams {
export interface UseReferenceManyFieldControllerParams<
RecordType extends RaRecord = RaRecord
> {
filter?: any;
page?: number;
perPage?: number;
record?: RaRecord;
record?: RecordType;
reference: string;
resource?: string;
sort?: SortPayload;
Expand Down Expand Up @@ -52,9 +54,12 @@ const defaultFilter = {};
*
* @returns {ListControllerResult} The reference many props
*/
export const useReferenceManyFieldController = (
props: UseReferenceManyFieldControllerParams
): ListControllerResult => {
export const useReferenceManyFieldController = <
RecordType extends RaRecord = RaRecord,
ReferenceRecordType extends RaRecord = RaRecord
>(
props: UseReferenceManyFieldControllerParams<RecordType>
): ListControllerResult<ReferenceRecordType> => {
const {
reference,
record,
Expand Down Expand Up @@ -147,11 +152,11 @@ export const useReferenceManyFieldController = (
isFetching,
isLoading,
refetch,
} = useGetManyReference(
} = useGetManyReference<ReferenceRecordType>(
reference,
{
target,
id: get(record, source),
id: get(record, source) as Identifier,
pagination: { page, perPage },
sort,
filter: filterValues,
Expand Down
18 changes: 9 additions & 9 deletions packages/ra-core/src/controller/list/useRecordSelection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react';

import { useStore, useRemoveFromStore } from '../../store';
import { Identifier } from '../../types';
import { RaRecord } from '../../types';

/**
* Get the list of selected items for a resource, and callbacks to change the selection
Expand All @@ -10,14 +10,14 @@ import { Identifier } from '../../types';
*
* @returns {Object} Destructure as [selectedIds, { select, toggle, clearSelection }].
*/
export const useRecordSelection = (
export const useRecordSelection = <RecordType extends RaRecord = any>(
resource: string
): [
Identifier[],
RecordType['id'][],
{
select: (ids: Identifier[]) => void;
unselect: (ids: Identifier[]) => void;
toggle: (id: Identifier) => void;
select: (ids: RecordType['id'][]) => void;
unselect: (ids: RecordType['id'][]) => void;
toggle: (id: RecordType['id']) => void;
clearSelection: () => void;
}
] => {
Expand All @@ -27,18 +27,18 @@ export const useRecordSelection = (

const selectionModifiers = useMemo(
() => ({
select: (idsToAdd: Identifier[]) => {
select: (idsToAdd: RecordType['id'][]) => {
if (!idsToAdd) return;
setIds([...idsToAdd]);
},
unselect(idsToRemove: Identifier[]) {
unselect(idsToRemove: RecordType['id'][]) {
if (!idsToRemove || idsToRemove.length === 0) return;
setIds(ids => {
if (!Array.isArray(ids)) return [];
return ids.filter(id => !idsToRemove.includes(id));
});
},
toggle: (id: Identifier) => {
toggle: (id: RecordType['id']) => {
if (typeof id === 'undefined') return;
setIds(ids => {
if (!Array.isArray(ids)) return [...ids];
Expand Down
5 changes: 3 additions & 2 deletions packages/ra-core/src/controller/record/useRecordContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext } from 'react';
import { RecordContext } from './RecordContext';
import { RaRecord } from '../../types';

/**
* Hook to read the record from a RecordContext.
Expand Down Expand Up @@ -30,7 +31,7 @@ import { RecordContext } from './RecordContext';
* @returns {RaRecord} A record object
*/
export const useRecordContext = <
RecordType extends Record<string, unknown> = Record<string, any>
RecordType extends RaRecord | Omit<RaRecord, 'id'> = RaRecord
>(
props?: UseRecordContextParams<RecordType>
): RecordType | undefined => {
Expand All @@ -42,7 +43,7 @@ export const useRecordContext = <
};

export interface UseRecordContextParams<
RecordType extends Record<string, unknown> = Record<string, unknown>
RecordType extends RaRecord | Omit<RaRecord, 'id'> = RaRecord
> {
record?: RecordType;
[key: string]: any;
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface UseReferenceResult<RecordType extends RaRecord = any> {
*
* @returns {UseReferenceResult} The reference record
*/
export const useReference = <RecordType extends RaRecord = any>({
export const useReference = <RecordType extends RaRecord = RaRecord>({
reference,
id,
options = {},
Expand Down
Loading