-
Notifications
You must be signed in to change notification settings - Fork 36
/
Copy pathresource.tsx
149 lines (138 loc) · 4.74 KB
/
resource.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import React from 'react';
import { SearchDialogContext } from '../components/Core/Contexts';
import { fetchDistantRelated } from '../components/DataModel/helpers';
import type {
AnySchema,
SerializedResource,
} from '../components/DataModel/helperTypes';
import type { SpecifyResource } from '../components/DataModel/legacyTypes';
import { resourceOn } from '../components/DataModel/resource';
import { serializeResource } from '../components/DataModel/serializers';
import type {
LiteralField,
Relationship,
} from '../components/DataModel/specifyField';
import { raise } from '../components/Errors/Crash';
import type { Parser } from '../utils/parser/definitions';
import { mergeParsers, resolveParser } from '../utils/parser/definitions';
import type { GetOrSet, RA } from '../utils/types';
import { useLiveState } from './useLiveState';
/**
* A wrapper for Backbone.Resource that integrates with React.useState for
* easier state tracking
*
* @example Can detect field changes using React hooks:
* React.useEffect(()=>{}, [resource]);
* @example Or only certain fields:
* React.useEffect(()=>{}, [resource.name, resource.fullname]);
*/
export function useResource<SCHEMA extends AnySchema>(
table: SpecifyResource<SCHEMA>
): GetOrSet<SerializedResource<SCHEMA>> {
const [resource, setResource] = React.useState<SerializedResource<SCHEMA>>(
() => serializeResource(table)
);
const isChanging = React.useRef<boolean>(false);
React.useEffect(() =>
resourceOn(
table,
'change',
() => {
if (isChanging.current) return;
const newResource = serializeResource(table);
previousResourceRef.current = newResource;
setResource(newResource);
},
false
)
);
const previousResourceRef =
React.useRef<SerializedResource<SCHEMA>>(resource);
const previousTable = React.useRef(table);
React.useEffect(() => {
if (previousTable.current !== table) {
previousTable.current = table;
const newResource = serializeResource(table);
previousResourceRef.current = newResource;
setResource(newResource);
return;
}
const changes = Object.entries(resource).filter(
([key, newValue]) =>
(newValue as unknown) !== previousResourceRef.current[key]
);
if (changes.length === 0) return;
isChanging.current = true;
changes.forEach(([key, newValue]) =>
table.set(key as 'resource_uri', newValue as never)
);
isChanging.current = false;
previousResourceRef.current = resource;
}, [resource, table]);
return [resource, setResource];
}
/**
* I.e, if you have a Collection Object resource and the following fields:
* [accession, accessionNumber], this function will fetch the accession and
* return accession and accession number field. Basically, it climbs the fields
* until it gets to the last field and corresponding resource, which are
* returned.
*
* If collection object has no accession, undefined is returned.
*/
export function useDistantRelated(
resource: SpecifyResource<AnySchema>,
fields: RA<LiteralField | Relationship> | undefined
): Awaited<ReturnType<typeof fetchDistantRelated>> {
const [data, setData] =
React.useState<Awaited<ReturnType<typeof fetchDistantRelated>>>(undefined);
React.useEffect(() => {
let destructorCalled = false;
const handleChange = (): void =>
void fetchDistantRelated(resource, fields)
.then((data) => (destructorCalled ? undefined : setData(data)))
.catch(raise);
if (fields === undefined || fields.length === 0) {
handleChange();
return undefined;
}
const destructor = resourceOn(
resource,
`change:${fields[0].name}`,
handleChange,
true
);
return (): void => {
destructor();
destructorCalled = true;
};
}, [resource, fields]);
return data;
}
export function useParser(
field: LiteralField | Relationship | undefined,
resource?: SpecifyResource<AnySchema> | undefined,
defaultParser?: Parser
): Parser {
const isInSearchDialog = React.useContext(SearchDialogContext);
const formatter =
field?.isRelationship === false
? field.getUiFormatter(resource)
: undefined;
return useLiveState<Parser>(
React.useCallback(() => {
/*
* Disable parser when in search dialog as space and quote characters are
* interpreted differently in them thus validation for them should be
* disabled.
*/
const parser =
isInSearchDialog || field === undefined
? { type: 'text' as const }
: resolveParser(field, undefined, resource);
return typeof defaultParser === 'object'
? mergeParsers(parser, defaultParser)
: parser;
}, [isInSearchDialog, field, resource, formatter, defaultParser])
)[0];
}