diff --git a/specifyweb/context/urls.py b/specifyweb/context/urls.py index 428d960d996..3ff82c9344c 100644 --- a/specifyweb/context/urls.py +++ b/specifyweb/context/urls.py @@ -20,6 +20,7 @@ url(r'^api_endpoints_all.json$', views.api_endpoints_all), url(r'^user.json$', views.user), url(r'^system_info.json$', views.system_info), + url(r'^all_system_data.json$', views.all_system_data), url(r'^domain.json$', views.domain), url(r'^view.json$', views.view), url(r'^views.json$', views.views), diff --git a/specifyweb/context/views.py b/specifyweb/context/views.py index dddc0330a70..5bb7075b665 100644 --- a/specifyweb/context/views.py +++ b/specifyweb/context/views.py @@ -25,7 +25,7 @@ PermissionTargetAction, \ check_permission_targets, skip_collection_access_check, query_pt, \ CollectionAccessPT -from specifyweb.specify.models import Collection, Institution, \ +from specifyweb.specify.models import Collection, Discipline, Division, Institution, \ Specifyuser, Spprincipal, Spversion, Collectionobjecttype from specifyweb.specify.schema import base_schema from specifyweb.specify.api import uri_for_model @@ -654,6 +654,53 @@ def system_info(request): ) return HttpResponse(json.dumps(info), content_type='application/json') +@require_http_methods(["GET"]) +@cache_control(max_age=86400, public=True) +@skip_collection_access_check +def all_system_data(request): + """ + Returns all institutions, divisions, disciplines, and collections. + """ + institution = Institution.objects.get() + divisions = list(Division.objects.all()) + disciplines = list(Discipline.objects.all()) + collections = list(Collection.objects.all()) + + discipline_map = {} + for discipline in disciplines: + discipline_map[discipline.id] = { + "id": discipline.id, + "name": discipline.name, + "children": [] + } + + for collection in collections: + if collection.discipline_id in discipline_map: + discipline_map[collection.discipline_id]["children"].append({ + "id": collection.id, + "name": collection.collectionname + }) + + division_map = {} + for division in divisions: + division_map[division.id] = { + "id": division.id, + "name": division.name, + "children": [] + } + + for discipline in disciplines: + if discipline.division_id in division_map: + division_map[discipline.division_id]["children"].append(discipline_map[discipline.id]) + + institution_data = { + "id": institution.id, + "name": institution.name, + "children": list(division_map.values()) + } + + return JsonResponse(institution_data, safe=False) + PATH_GROUP_RE = re.compile(r'\(\?P<([^>]+)>[^\)]*\)') PATH_GROUP_RE_EXTENDED = re.compile(r'<([^:]+):([^>]+)>') diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/webOnlyViews.ts b/specifyweb/frontend/js_src/lib/components/FormParse/webOnlyViews.ts index 6d0c43f68d3..6ba8a0cfe74 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/webOnlyViews.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/webOnlyViews.ts @@ -84,6 +84,11 @@ export const webOnlyViews = f.store(() => 'edit', ['name'] ), + [collection]: autoGenerateViewDefinition( + tables.Collection, + 'form', + 'edit', + ['collectionName', 'code', 'catalogNumFormatName']) } as const) ); @@ -92,3 +97,4 @@ export const attachmentView = 'ObjectAttachment'; export const spAppResourceView = '_SpAppResourceView_name'; export const spViewSetNameView = '_SpViewSetObj_name'; export const recordSetView = '_RecordSet_name'; +export const collection = '_Collection_setup'; diff --git a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts index cc1172d414d..d47cd7f1cdf 100644 --- a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts @@ -79,6 +79,11 @@ const rawUserTools = ensure>>>()({ url: '/specify/security/', icon: icons.fingerPrint, }, + systemConfigurationTool: { + title: userText.systemConfigurationTool(), + url: '/specify/system-configuration/', + icon: icons.fingerPrint, + }, repairTree: { title: headerText.repairTree(), url: '/specify/overlay/tree-repair/', diff --git a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx index e7fef4d8f33..a6565c4c7d0 100644 --- a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx +++ b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx @@ -174,6 +174,12 @@ export const routes: RA = [ }, ], }, + { + path: 'system-configuration', + title: userText.securityPanel(), + element: () => + import('../Toolbar/SystemConfigTool').then(({ SystemConfigurationTool }) => SystemConfigurationTool), + }, { path: 'attachments', children: [ diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx new file mode 100644 index 00000000000..e8bf9029654 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx @@ -0,0 +1,161 @@ +import React from 'react'; + +import { useBooleanState } from '../../hooks/useBooleanState'; +import { userText } from '../../localization/user'; +import type { RA } from '../../utils/types'; +import { Container, H2, H3, Ul } from '../Atoms'; +import { Button } from '../Atoms/Button'; +import type { SpecifyResource } from '../DataModel/legacyTypes'; +import { tables } from '../DataModel/tables'; +import type { Collection } from '../DataModel/types'; +import { collection } from '../FormParse/webOnlyViews'; +import { ResourceView } from '../Forms/ResourceView'; +import { load } from '../InitialContext'; +import { Dialog, LoadingScreen } from '../Molecules/Dialog'; + +export function SystemConfigurationTool(): JSX.Element | null { + const [allInfo, setAllInfo] = React.useState(null); + + const [newResourceOpen, handleNewResource, closeNewResource] = useBooleanState() + + const [parentId, setParentId] = React.useState() + + const [newResource, setNewResource] = React.useState | undefined>() + + React.useEffect(() => { + fetchAllSystemData.then(setAllInfo).catch(() => console.warn('Error when fetching institution info')); + }, []); + + const renderHierarchy = (institution: InstitutionData | null): JSX.Element => { + if (!institution) return ; + + return ( +
    +
  • +
    +

    {`Institution: ${institution.name}`}

    + { + console.log(`'Add new division to institution' ${institution.id}`) + setParentId(institution.id) + setNewResource(new tables.Division.Resource()) + handleNewResource() + }} + /> +
    +
      + {institution.children.map((division) => ( +
    • +
      +

      {`Division: ${division.name}`}

      + { + console.log(`'Add new discipline to division' ${division.id}`) + setParentId(division.id) + setNewResource(new tables.Discipline.Resource()) + handleNewResource() + }} + /> +
      + {division.children.length > 0 && ( +
        + {division.children.map((discipline) => ( +
      • +
        +

        {`Discipline: ${discipline.name}`}

        + { + console.log(`'Add new collection to discipline' ${discipline.id}`) + setParentId(discipline.id) + setNewResource(new tables.Collection.Resource()) + handleNewResource() + }}/> +
        + {discipline.children.length > 0 && ( +
          + {discipline.children.map((collection) => ( +
        • +

          {collection.name}

          +
        • + ))} +
        + )} +
      • + ))} +
      + )} + +
    • + ))} +
    +
  • +
+ ); + }; + + return ( + +

{userText.systemConfigurationTool()}

+
+ {allInfo === undefined || allInfo === null ? : renderHierarchy(allInfo)} +
+ {newResourceOpen ? + + console.log('Click')} + /> + + : undefined} +
+ ); +} + +type InstitutionData = { + /* + * Readonly 'institution': { + * // Institution + */ + readonly id: number; + readonly name: string; + readonly children: RA<{ + // Division + readonly id: number; + readonly name: string; + readonly children: RA<{ + // Discipline + readonly id: number; + readonly name: string; + readonly children: RA<{ + // Collection + readonly id: number; + readonly name: string; + }>; + }>; + }>;} +// }; + +let institutionData: InstitutionData; + +export const fetchAllSystemData = load( + '/context/all_system_data.json', + 'application/json' +).then((data: InstitutionData) => { + institutionData = data; + return data; +}); + +export const getAllInfo = ():InstitutionData => institutionData \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/localization/user.ts b/specifyweb/frontend/js_src/lib/localization/user.ts index 14f22163069..a3f5ba39337 100644 --- a/specifyweb/frontend/js_src/lib/localization/user.ts +++ b/specifyweb/frontend/js_src/lib/localization/user.ts @@ -624,6 +624,9 @@ export const userText = createDictionary({ 'de-ch': 'Sicherheit und Konten', 'ru-ru': 'Безопасность и учетные записи', }, + systemConfigurationTool: { + 'en-us': 'System Configuration Tool', + }, userRoleLibrary: { 'en-us': 'Institution Library of Role Templates', 'ru-ru': 'Библиотека шаблонов ролей учреждения',