Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion specifyweb/backend/context/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,13 @@ def user(request):
obj_to_data(c)
for c in available_collections
]
data['agent'] = obj_to_data(request.specify_user_agent) if request.specify_user_agent != None else None
current_agent = (
obj_to_data(request.specify_user_agent)
if request.specify_user_agent is not None
else None
)
data['agent'] = current_agent
data['current_agent'] = current_agent

if settings.RO_MODE or not request.user.is_authenticated:
data['usertype'] = "readonly"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,18 @@ describe('AppResourceEditButton', () => {

const dialog = getByRole('dialog');
expect(dialog.innerHTML).toMatchInlineSnapshot(`
"<div class=\\"
flex items-center gap-2 md:gap-4
-m-4 cursor-move p-4
flex-wrap
\\" id=\\"modal-0-handle\\"><div class=\\"flex items-center gap-2\\"><h2 class=\\"font-semibold text-black dark:text-white text-xl\\" id=\\"modal-0-header\\">TestTitle</h2></div></div><div class=\\"
dark:text-neutral-350 -mx-1 flex-1 overflow-y-auto px-1 py-4
text-gray-700 flex flex-col gap-2
\\" id=\\"modal-0-content\\"></div><div class=\\"flex gap-2 justify-end\\"><span class=\\"-ml-2 flex-1\\"></span><button class=\\"button rounded cursor-pointer active:brightness-80 px-4 py-2
disabled:bg-gray-200 disabled:dark:ring-neutral-500 disabled:ring-gray-400 disabled:text-gray-500
dark:disabled:!bg-neutral-700 gap-2 inline-flex items-center capitalize justify-center shadow-sm button hover:brightness-90 dark:hover:brightness-125 bg-[color:var(--secondary-button-color)] text-gray-800
dark:text-gray-100\\" type=\\"button\\">Close</button></div>"
`);
"<div class=\\"
flex items-center gap-2 md:gap-4
-m-4 cursor-move p-4
flex-wrap
\\" id=\\"modal-0-handle\\"><div class=\\"flex items-center gap-2\\"><h2 class=\\"font-semibold text-black dark:text-white text-xl\\" id=\\"modal-0-header\\">TestTitle</h2></div></div><div class=\\"
dark:text-neutral-350 -mx-1 flex-1 overflow-y-auto px-1 py-4
text-gray-700 flex flex-col gap-2
\\" id=\\"modal-0-content\\"></div><div class=\\"flex gap-2 justify-end\\"><span class=\\"-ml-2 flex-1\\"></span><button class=\\"button rounded cursor-pointer active:brightness-80 px-4 py-2
disabled:bg-gray-200 disabled:dark:ring-neutral-500 disabled:ring-gray-400 disabled:text-gray-500
dark:disabled:!bg-neutral-700 gap-2 inline-flex items-center capitalize justify-center shadow-sm button hover:brightness-90 dark:hover:brightness-125 bg-[color:var(--secondary-button-color)] text-gray-800
dark:text-gray-100\\" type=\\"button\\">Close</button></div>"
`);

expect(handleDeleted).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ describe('uploadFile', () => {
return {
open: jest.fn(),
send: jest.fn((..._args: readonly unknown[]) => listeners[nextEvent]?.()),
addEventListener: jest.fn((eventName: EventName, callback: () => void) => {
listeners[eventName] = callback;
}),
addEventListener: jest.fn(
(eventName: EventName, callback: () => void) => {
listeners[eventName] = callback;
}
),
removeEventListener: jest.fn(
(eventName: EventName, callback: () => void) => {
if (listeners[eventName] === callback) listeners[eventName] = undefined;
if (listeners[eventName] === callback)
listeners[eventName] = undefined;
}
),
upload: {
Expand Down
4 changes: 3 additions & 1 deletion specifyweb/frontend/js_src/lib/components/Core/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export function Main({
}: {
readonly menuItems: RA<MenuItem>;
}): JSX.Element {
const [hasAgent] = React.useState(userInformation.agent !== null);
const [hasAgent] = React.useState(
userInformation.currentCollectionAgent !== null
);

const mainRef = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function handleAjaxError(
* If exceptions occur because user has no agent, don't display the error
* message, so as not to spawn a new dialog on top of the "No Agent" dialog
*/
if (userInformation.agent === null) throw error;
if (userInformation.currentCollectionAgent === null) throw error;

if (errorMode !== 'silent') {
const isNotFoundError =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,29 @@ export type UserInformation = SerializedRecord<SpecifyUser> & {
readonly name: LocalizedString;
readonly isauthenticated: boolean;
readonly availableCollections: RA<SerializedResource<Collection>>;
readonly agent: SerializedRecord<Agent>;
readonly currentCollectionAgent: SerializedRecord<Agent> | null;
readonly agent: SerializedRecord<Agent> | null;
};

const userInfo: Writable<UserInformation> = {} as UserInformation;

export const fetchContext = load<
Omit<UserInformation, 'availableCollections'> & {
Omit<UserInformation, 'availableCollections' | 'currentCollectionAgent'> & {
// eslint-disable-next-line @typescript-eslint/naming-convention
readonly available_collections: RA<SerializedRecord<Collection>>;
readonly current_agent?: SerializedRecord<Agent> | null;
}
>('/context/user.json', 'application/json').then(
async ({ available_collections: availableCollections, ...data }) => {
Object.entries(data).forEach(([key, value]) => {
const currentCollectionAgent = data.current_agent ?? data.agent ?? null;
const rest = { ...data };
delete rest.current_agent;
Object.entries(rest).forEach(([key, value]) => {
// @ts-expect-error
userInfo[key as keyof UserInformation] = value;
});
userInfo.currentCollectionAgent = currentCollectionAgent;
userInfo.agent = currentCollectionAgent;
await import('../DataModel/tables').then(
async ({ fetchContext }) => fetchContext
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function PreparationReturn({
const loanReturnPreparation = React.useRef(
new tables.LoanReturnPreparation.Resource({
returneddate: getDateInputValue(new Date()),
receivedby: userInformation.agent.resource_uri,
receivedby: userInformation.currentCollectionAgent?.resource_uri,
})
);
const [state, setState] = React.useState<RA<PrepReturnRowState>>(() =>
Expand Down
6 changes: 3 additions & 3 deletions specifyweb/frontend/js_src/lib/components/Molecules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import React from 'react';

import { useHueDifference } from '../../hooks/useHueDifference';
import { commonText } from '../../localization/common';
import type { RA } from '../../utils/types';
import { useHueDifference } from '../../hooks/useHueDifference';

export const loadingGif = (
<div className="hover:animate-hue-rotate [.reduce-motion_&]:animate-hue-rotate">
Expand All @@ -31,7 +31,7 @@ export const loadingGif = (
* This must be accompanied by a label since loading bar is hidden from screen
* readers
*/
const LoadingBar = () => {
function LoadingBar() {
const hueDifference = useHueDifference();

return (
Expand All @@ -45,7 +45,7 @@ const LoadingBar = () => {
/>
</div>
);
};
}

export const loadingBar = <LoadingBar />;

Expand Down
13 changes: 8 additions & 5 deletions specifyweb/frontend/js_src/lib/components/Permissions/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ export function hasTablePermission(
): boolean {
const isReadOnly = getCache('forms', 'readOnlyMode') ?? false;
if (isReadOnly && action !== 'read') return false;
const permissionsForCollection = getTablePermissions()[collectionId];
if (
getTablePermissions()[collectionId][tableNameToResourceName(tableName)][
action
]
permissionsForCollection?.[tableNameToResourceName(tableName)]?.[action]
)
return true;
if (permissionsForCollection === undefined) {
f.log(`Permissions for collection ${collectionId} are not loaded`);
return false;
}
console.log(`No permission to ${action} ${tableName}`);
return false;
}
Expand All @@ -49,7 +52,7 @@ export const hasPermission = <
): boolean =>
resource === '%' && action === '%'
? userInformation.isadmin
: getOperationPermissions()[collectionId][resource][action]
: getOperationPermissions()[collectionId]?.[resource]?.[action]
? true
: (f.log(`No permission to ${action.toString()} ${resource}`) ?? false);

Expand Down Expand Up @@ -78,7 +81,7 @@ export const hasDerivedPermission = <
action: keyof ReturnType<typeof getDerivedPermissions>[number][RESOURCE],
collectionId = schema.domainLevelIds.collection
): boolean =>
getDerivedPermissions()[collectionId][resource][action]
getDerivedPermissions()[collectionId]?.[resource]?.[action]
? true
: (f.log(`No permission to ${action.toString()} ${resource}`) ?? false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function QueryLoanReturn({
type: 'Dialog',
loanReturnPreparation: new tables.LoanReturnPreparation.Resource({
returneddate: getDateInputValue(new Date()),
receivedby: userInformation.agent.resource_uri,
receivedby: userInformation.currentCollectionAgent?.resource_uri,
}),
queryResource: resourceToJson(
typeof getQueryFieldRecords === 'function'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ test('Query Combo Box with current agent selected', () => {
field: tables.CollectionObject.strictGetRelationship('cataloger'),
defaultRecord,
});
expect(resource.get('cataloger')).toBe(userInformation.agent.resource_uri);
expect(resource.get('cataloger')).toBe(
userInformation.currentCollectionAgent?.resource_uri
);
});
test('Query Combo Box with current user selected', () => {
const resource = new tables.RecordSet.Resource();
Expand Down
72 changes: 68 additions & 4 deletions specifyweb/frontend/js_src/lib/components/QueryComboBox/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { RA, WritableArray } from '../../utils/types';
import { toTable, toTreeTable } from '../DataModel/helpers';
import type { AnySchema } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import { idFromUrl, strictIdFromUrl } from '../DataModel/resource';
import {
fetchResource,
getResourceApiUrl,
idFromUrl,
strictIdFromUrl,
} from '../DataModel/resource';
import type { Relationship } from '../DataModel/specifyField';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { tables } from '../DataModel/tables';
Expand Down Expand Up @@ -232,8 +237,65 @@ export function pendingValueToResource(
);
}

/**
* QueryComboBox can be rendered for a collection other than the currently
* logged-in one, via forceCollection. New resources are initialized with the
* current domain by default, so override scope to the target collection
*/
export async function scopeNewResourceToCollection(
resource: SpecifyResource<AnySchema>,
collectionId: number | undefined
): Promise<SpecifyResource<AnySchema>> {
if (!resource.isNew() || typeof collectionId !== 'number') return resource;

const scopingRelationship = resource.specifyTable.getScopingRelationship();
if (scopingRelationship === undefined) return resource;

const scopeTableName = scopingRelationship.relatedTable.name;
try {
const disciplineUrl =
userInformation.availableCollections.find(({ id }) => id === collectionId)
?.discipline ??
(scopeTableName === 'Collection'
? undefined
: (await fetchResource('Collection', collectionId)).discipline);
const discipline =
scopeTableName === 'Division' || scopeTableName === 'Institution'
? typeof disciplineUrl === 'string'
? await fetchResource('Discipline', strictIdFromUrl(disciplineUrl))
: undefined
: undefined;
const divisionUrl = discipline?.division;
const division =
scopeTableName === 'Institution'
? typeof divisionUrl === 'string'
? await fetchResource('Division', strictIdFromUrl(divisionUrl))
: undefined
: undefined;

const targetScopeUrl =
scopeTableName === 'Collection'
? getResourceApiUrl('Collection', collectionId)
: scopeTableName === 'Discipline'
? disciplineUrl
: scopeTableName === 'Division'
? divisionUrl
: scopeTableName === 'Institution'
? division?.institution
: undefined;

if (typeof targetScopeUrl === 'string')
resource.set(scopingRelationship.name, targetScopeUrl as never);
} catch {
// Fall back to default scoping if the lookup fails
}

return resource;
}

const DEFAULT_RECORD_PRESETS = {
CURRENT_AGENT: () => userInformation.agent.resource_uri,
CURRENT_AGENT: () =>
userInformation.currentCollectionAgent?.resource_uri ?? null,
CURRENT_USER: () => userInformation.resource_uri,
BLANK: () => null,
} as const;
Expand Down Expand Up @@ -264,7 +326,8 @@ export function useQueryComboBoxDefaults({
const record = toTable(resource, 'CollectionObject');
record?.set(
'cataloger',
record?.get('cataloger') ?? userInformation.agent.resource_uri,
record?.get('cataloger') ??
userInformation.currentCollectionAgent?.resource_uri,
{
silent: true,
}
Expand All @@ -279,7 +342,8 @@ export function useQueryComboBoxDefaults({
const record = toTable(resource, 'LoanReturnPreparation');
record?.set(
'receivedBy',
record?.get('receivedBy') ?? userInformation.agent.resource_uri,
record?.get('receivedBy') ??
userInformation.currentCollectionAgent?.resource_uri,
{
silent: true,
}
Expand Down
Loading
Loading