Skip to content

Commit 22ed268

Browse files
committed
Merge remote-tracking branch 'origin/production' into issue-4685
2 parents 049960a + cab9ca5 commit 22ed268

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1233
-920
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ If applicable, add screenshots to help explain your problem.
2929
If the bug resulted in an error message, please click on "Download Error
3030
Message" and attach it here
3131

32-
Alternatively, please fill out the following information manually:
32+
Please, also fill out the following information manually:
3333

3434
- OS: [e.g. Windows 10]
3535
- Browser: [e.g. Chrome, Safari]
3636
- Specify 7 Version: [e.g. 7.6.1]
3737
- Database Name: [e.g. kufish] (you can see this in the "About Specify 7" dialog)
3838
- Collection name: [e.g. KU Fish Tissue]
39+
- User Name: [e.g. SpAdmin]
3940

4041
**Reported By**
4142
Name of your institution

specifyweb/frontend/js_src/lib/components/AppResources/EditorComponents.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,11 @@ export function useCodeMirrorExtensions(
226226
function handleLinted(results: RA<Diagnostic>, view: EditorView): void {
227227
if (isFirstLint && results.length > 0) {
228228
isFirstLint = false;
229-
setTimeout(() => openLintPanel(view), 0);
229+
setTimeout(() => {
230+
const currentFocus = document.activeElement as HTMLElement | null;
231+
openLintPanel(view);
232+
currentFocus?.focus();
233+
}, 0);
230234
}
231235
setBlockers(
232236
filterArray(

specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getAppResourceMode } from './helpers';
2929
import type { AppResources, AppResourcesTree } from './hooks';
3030
import { useResourcesTree } from './hooks';
3131
import type { AppResourcesOutlet } from './index';
32+
import { globalResourceKey } from './tree';
3233
import type { ScopedAppResourceDir } from './types';
3334
import { appResourceSubTypes } from './types';
3435

@@ -98,10 +99,10 @@ export function Wrapper({
9899
onClone={(clonedResource, clone): void =>
99100
navigate(
100101
formatUrl(`${baseHref}/new/`, {
101-
directoryKey: findAppResourceDirectoryKey(
102-
resourcesTree,
103-
directory.id
104-
),
102+
directoryKey:
103+
directory.scope === 'global'
104+
? globalResourceKey
105+
: findAppResourceDirectoryKey(resourcesTree, directory.id),
105106
name: clonedResource.name,
106107
mimeType: 'mimeType' in record ? record.mimeType : undefined,
107108
clone,

specifyweb/frontend/js_src/lib/components/AppResources/tree.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { userTypes } from '../PickLists/definitions';
1717
import type { AppResources, AppResourcesTree } from './hooks';
1818
import type { AppResourceScope, ScopedAppResourceDir } from './types';
1919

20+
export const globalResourceKey = 'globalResource';
21+
2022
export const getScope = (
2123
directory: SerializedResource<SpAppResourceDir>
2224
): AppResourceScope => {
@@ -37,7 +39,7 @@ export const getAppResourceTree = (
3739
): AppResourcesTree => [
3840
{
3941
label: resourcesText.globalResources(),
40-
key: 'globalResources',
42+
key: globalResourceKey,
4143
...getGlobalAllResources(resources),
4244
subCategories: [],
4345
},
@@ -285,7 +287,6 @@ const getUserResources = (
285287
specifyUser: user.resource_uri,
286288
isPersonal: true,
287289
});
288-
289290
return {
290291
label: localized(user.name),
291292
key: `collection_${collection.id}_user_${user.id}`,

specifyweb/frontend/js_src/lib/components/Atoms/Form.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export const Input = {
136136
'input',
137137
{
138138
readonly onValueChange?: (isChecked: boolean) => void;
139+
readonly onClick?: 'Use onValueChange instead';
139140
readonly readOnly?: 'Use isReadOnly instead';
140141
readonly isReadOnly?: boolean;
141142
readonly type?: 'If you need to specify type, use Input.Generic';

specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts

+127
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { renderHook } from '@testing-library/react';
33
import { overrideAjax } from '../../../tests/ajax';
44
import { mockTime, requireContext } from '../../../tests/helpers';
55
import { overwriteReadOnly } from '../../../utils/types';
6+
import type { SerializedResource } from '../helperTypes';
67
import { getResourceApiUrl } from '../resource';
78
import { useSaveBlockers } from '../saveBlockers';
89
import { schema } from '../schema';
910
import { tables } from '../tables';
11+
import type { Taxon, TaxonTreeDefItem } from '../types';
1012

1113
mockTime();
1214
requireContext();
@@ -180,3 +182,128 @@ describe('uniqueness rules', () => {
180182
]);
181183
});
182184
});
185+
186+
describe('treeBusinessRules', () => {
187+
const animaliaResponse: Partial<SerializedResource<Taxon>> = {
188+
_tableName: 'Taxon',
189+
id: 2,
190+
fullName: 'Animalia',
191+
parent: '/api/specify/taxon/1/',
192+
definition: '/api/specify/taxontreedef/1/',
193+
definitionItem: '/api/specify/taxontreedefitem/21/',
194+
rankId: 10,
195+
};
196+
const acipenserResponse: Partial<SerializedResource<Taxon>> = {
197+
_tableName: 'Taxon',
198+
id: 3,
199+
name: 'Acipenser',
200+
rankId: 180,
201+
definition: '/api/specify/taxontreedef/1/',
202+
definitionItem: '/api/specify/taxontreedefitem/9/',
203+
parent: '/api/specify/taxon/2/',
204+
};
205+
206+
const oxyrinchusSpeciesResponse: Partial<SerializedResource<Taxon>> = {
207+
_tableName: 'Taxon',
208+
id: 4,
209+
name: 'oxyrinchus',
210+
rankId: 220,
211+
definition: '/api/specify/taxontreedef/1/',
212+
definitionItem: '/api/specify/taxontreedefitem/2/',
213+
parent: '/api/specify/taxon/3/',
214+
};
215+
216+
const oxyrinchusSubSpeciesResponse: Partial<SerializedResource<Taxon>> = {
217+
_tableName: 'Taxon',
218+
id: 5,
219+
rankId: 230,
220+
name: 'oxyrinchus',
221+
definition: '/api/specify/taxontreedef/1/',
222+
definitionItem: '/api/specify/taxontreedefitem/22/',
223+
};
224+
225+
const genusResponse: Partial<SerializedResource<TaxonTreeDefItem>> = {
226+
_tableName: 'TaxonTreeDefItem',
227+
id: 9,
228+
fullNameSeparator: ' ',
229+
isEnforced: true,
230+
isInFullName: true,
231+
name: 'Genus',
232+
rankId: 180,
233+
title: 'Genus',
234+
parent: '/api/specify/taxontreedefitem/8/',
235+
treeDef: '/api/specify/taxontreedef/1/',
236+
resource_uri: '/api/specify/taxontreedefitem/9/',
237+
};
238+
239+
const speciesResponse: Partial<SerializedResource<TaxonTreeDefItem>> = {
240+
id: 2,
241+
fullNameSeparator: ' ',
242+
isEnforced: false,
243+
isInFullName: true,
244+
name: 'Species',
245+
rankId: 220,
246+
title: null,
247+
version: 2,
248+
parent: '/api/specify/taxontreedefitem/15/',
249+
treeDef: '/api/specify/taxontreedef/1/',
250+
resource_uri: '/api/specify/taxontreedefitem/2/',
251+
};
252+
253+
const oxyrinchusFullNameResponse = 'Acipenser oxyrinchus';
254+
255+
overrideAjax('/api/specify/taxon/2/', animaliaResponse);
256+
overrideAjax('/api/specify/taxon/3/', acipenserResponse);
257+
overrideAjax('/api/specify/taxon/4/', oxyrinchusSpeciesResponse);
258+
overrideAjax('/api/specify/taxon/5/', oxyrinchusSubSpeciesResponse);
259+
overrideAjax('/api/specify/taxontreedefitem/9/', genusResponse);
260+
overrideAjax('/api/specify/taxontreedefitem/2/', speciesResponse);
261+
overrideAjax(
262+
'/api/specify_tree/taxon/3/predict_fullname/?name=oxyrinchus&treedefitemid=2',
263+
oxyrinchusFullNameResponse
264+
);
265+
266+
test('fullName being set', async () => {
267+
const oxyrinchus = new tables.Taxon.Resource({
268+
_tableName: 'Taxon',
269+
id: 4,
270+
});
271+
await oxyrinchus.fetch();
272+
await oxyrinchus.businessRuleManager?.checkField('parent');
273+
expect(oxyrinchus.get('fullName')).toBe('Acipenser oxyrinchus');
274+
});
275+
276+
test('parent blocking on invalid rank', async () => {
277+
const taxon = new tables.Taxon.Resource({
278+
name: 'Ameiurus',
279+
parent: '/api/specify/taxon/3/',
280+
rankid: 180,
281+
definition: '/api/specify/taxontreedef/1/',
282+
definitionitem: '/api/specify/taxontreedefitem/9/',
283+
});
284+
285+
await taxon.businessRuleManager?.checkField('parent');
286+
287+
const { result } = renderHook(() =>
288+
useSaveBlockers(taxon, tables.Taxon.getField('parent'))
289+
);
290+
expect(result.current[0]).toStrictEqual(['Bad tree structure.']);
291+
});
292+
293+
test('parent blocking on invalid parent', async () => {
294+
const taxon = new tables.Taxon.Resource({
295+
name: 'Ameiurus',
296+
parent: '/api/specify/taxon/5/',
297+
rankid: 180,
298+
definition: '/api/specify/taxontreedef/1/',
299+
definitionitem: '/api/specify/taxontreedefitem/9/',
300+
});
301+
302+
await taxon.businessRuleManager?.checkField('parent');
303+
304+
const { result } = renderHook(() =>
305+
useSaveBlockers(taxon, tables.Taxon.getField('parent'))
306+
);
307+
expect(result.current[0]).toStrictEqual(['Bad tree structure.']);
308+
});
309+
});

specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,11 @@ export class BusinessRuleManager<SCHEMA extends AnySchema> {
162162
const field = this.resource.specifyTable.strictGetField(fieldName);
163163

164164
results.forEach((result) => {
165-
const ruleId = result.payload?.ruleId;
166165
const saveBlockerMessage = result.isValid ? [] : [result.reason];
167166
const saveBlockerKey =
168-
ruleId === undefined
167+
result.saveBlockerKey === undefined
169168
? getFieldBlockerKey(field, 'business-rule')
170-
: `uniqueness-${ruleId}`;
169+
: result.saveBlockerKey;
171170

172171
setSaveBlockers(
173172
this.resource,
@@ -223,9 +222,7 @@ export class BusinessRuleManager<SCHEMA extends AnySchema> {
223222
): Promise<BusinessRuleResult<SCHEMA>> {
224223
const invalidResponse: BusinessRuleResult<SCHEMA> = {
225224
isValid: false,
226-
payload: {
227-
ruleId: rule.id!,
228-
},
225+
saveBlockerKey: `uniqueness-${rule.id!}`,
229226
reason: getUniqueInvalidReason(
230227
rule.scopes.map(
231228
(scope) =>
@@ -240,9 +237,7 @@ export class BusinessRuleManager<SCHEMA extends AnySchema> {
240237
};
241238
const validResponse: BusinessRuleResult<SCHEMA> = {
242239
isValid: true,
243-
payload: {
244-
ruleId: rule.id!,
245-
},
240+
saveBlockerKey: `uniqueness-${rule.id!}`,
246241
};
247242

248243
const getFieldValue = async (
@@ -335,10 +330,18 @@ export class BusinessRuleManager<SCHEMA extends AnySchema> {
335330
value: otherValue,
336331
} = otherFieldValues;
337332
const { id, cid, value } = fieldValues[fieldName];
333+
const field = this.resource.specifyTable.getField(fieldName);
338334
if (otherCid !== undefined && cid !== undefined && otherCid === cid)
339335
return true;
340336
if (otherId !== undefined && id !== undefined && otherId === id)
341337
return true;
338+
if (
339+
field !== undefined &&
340+
!(field.isRequired || field.localization.isrequired) &&
341+
(value === undefined || value === null)
342+
) {
343+
return false;
344+
}
342345
return (
343346
otherId === undefined &&
344347
otherCid === undefined &&
@@ -460,7 +463,7 @@ export function attachBusinessRules(
460463

461464
export type BusinessRuleResult<SCHEMA extends AnySchema = AnySchema> = {
462465
readonly localDuplicates?: RA<SpecifyResource<SCHEMA>>;
463-
readonly payload?: { readonly ruleId: number };
466+
readonly saveBlockerKey?: string;
464467
} & (
465468
| {
466469
readonly isValid: true;

specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -277,5 +277,6 @@ export const findUnclaimedBlocker = (
277277
},
278278
]) !== undefined
279279
);
280+
else if (currentListeners.length === 0) return true;
280281
else return false;
281282
});

0 commit comments

Comments
 (0)