From 7036068f8dca91cc14d025440292cb3e03bfa1ed Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:32:15 -0800 Subject: [PATCH 1/6] Create a system config page Fixes #6213 --- .../lib/components/Header/userToolDefinitions.ts | 5 +++++ .../js_src/lib/components/Router/Routes.tsx | 6 ++++++ .../lib/components/Toolbar/SystemConfigTool.tsx | 14 ++++++++++++++ .../frontend/js_src/lib/localization/user.ts | 3 +++ 4 files changed, 28 insertions(+) create mode 100644 specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx 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..54950582d12 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { userText } from '../../localization/user'; +import { Container, H2 } from '../Atoms'; + +export function SystemConfigurationTool(): JSX.Element | null { + + return ( + +

{userText.systemConfigurationTool()}

+
+ + ); +} \ 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': 'Библиотека шаблонов ролей учреждения', From f5cc78c396a90a96a81cea0f00360384513f3945 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:17:32 -0800 Subject: [PATCH 2/6] Add new route to retrieve all institution info --- specifyweb/context/urls.py | 1 + specifyweb/context/views.py | 51 +++++++++++- .../components/Toolbar/SystemConfigTool.tsx | 78 ++++++++++++++++++- 3 files changed, 127 insertions(+), 3 deletions(-) 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..4da902bbd16 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,55 @@ 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 = { + "institution": { + "id": institution.id, + "name": institution.name, + "children": list(division_map.values()) + } + } + + return JsonResponse({"institution": 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/Toolbar/SystemConfigTool.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx index 54950582d12..c0e7b4f9043 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx @@ -2,13 +2,87 @@ import React from 'react'; import { userText } from '../../localization/user'; import { Container, H2 } from '../Atoms'; +import { load } from '../InitialContext'; export function SystemConfigurationTool(): JSX.Element | null { + const [allInfo, setAllInfo] = React.useState(null); + + React.useEffect(() => { + fetchAllSystemData.then(setAllInfo); + }, []); + + const renderHierarchy = (data: InstitutionData | null): JSX.Element => { + if (!data) return

Loading...

; + return ( +
    +
  • + {data.institution.name} +
      + {data.institution.children.map((division) => ( +
    • + {division.name} + {division.children.length > 0 && ( +
        + {division.children.map((discipline) => ( +
      • + {discipline.name} + {discipline.children.length > 0 && ( +
          + {discipline.children.map((collection) => ( +
        • + - {collection.name} +
        • + ))} +
        + )} +
      • + ))} +
      + )} +
    • + ))} +
    +
  • +
+ ); + }; + return (

{userText.systemConfigurationTool()}

-
+
+ {allInfo === undefined || allInfo === null ? undefined : renderHierarchy(allInfo)} +
); -} \ No newline at end of file +} + +type InstitutionData = { + readonly id: number; + readonly name: string; + readonly children: readonly { + readonly id: number; + readonly name: string; + readonly children: readonly { + readonly id: number; + readonly name: string; + readonly children: readonly { + readonly id: number; + readonly name: string; + }[]; + }[]; + }[]; +}; + +let institutionData: InstitutionData; + +export const fetchAllSystemData = load<{ readonly institution: InstitutionData }>( + '/context/all_system_data.json', + 'application/json' +).then((data: { readonly institution: InstitutionData }) => { + institutionData = data.institution; + return institutionData; +}); + +export const getAllInfo = ():InstitutionData => institutionData \ No newline at end of file From 8f16183bafbe3d1ac7a7cb92b6ba4a30d7a653a6 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:04:57 -0800 Subject: [PATCH 3/6] Organize resources --- .../components/Toolbar/SystemConfigTool.tsx | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx index c0e7b4f9043..07bfbe9c834 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { userText } from '../../localization/user'; -import { Container, H2 } from '../Atoms'; +import { Container, H2, H3, Ul } from '../Atoms'; +import { Button } from '../Atoms/Button'; import { load } from '../InitialContext'; +import { LoadingScreen } from '../Molecules/Dialog'; export function SystemConfigurationTool(): JSX.Element | null { const [allInfo, setAllInfo] = React.useState(null); @@ -12,39 +14,60 @@ export function SystemConfigurationTool(): JSX.Element | null { }, []); const renderHierarchy = (data: InstitutionData | null): JSX.Element => { - if (!data) return

Loading...

; + if (!data) return ; return ( -
    +
    • - {data.institution.name} -
        +
        +

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

        + console.log(`'Add new division to institution' ${data.institution.id}`)} + /> +
        +
          {data.institution.children.map((division) => (
        • - {division.name} +
          +

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

          + console.log(`'Add new discipline to division' ${division.id}`)} + /> +
          {division.children.length > 0 && ( -
            +
              {division.children.map((discipline) => (
            • - {discipline.name} +
              +

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

              + console.log(`'Add new collection to discipline' ${discipline.id}`)}/> +
              {discipline.children.length > 0 && ( -
                +
                  {discipline.children.map((collection) => ( -
                • - - {collection.name} +
                • +

                  {collection.name}

                • ))} -
                +
              )}
            • ))} -
            +
          )} +
        • ))} -
        +
    • -
    +
); }; From 4bc6a18eae91c100fff8ea611ce43f58677bbc9d Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:50:03 -0800 Subject: [PATCH 4/6] Chnage types --- .../components/Toolbar/SystemConfigTool.tsx | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx index 07bfbe9c834..217a988cad6 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { userText } from '../../localization/user'; +import type { RA } from '../../utils/types'; import { Container, H2, H3, Ul } from '../Atoms'; import { Button } from '../Atoms/Button'; import { load } from '../InitialContext'; @@ -10,7 +11,7 @@ export function SystemConfigurationTool(): JSX.Element | null { const [allInfo, setAllInfo] = React.useState(null); React.useEffect(() => { - fetchAllSystemData.then(setAllInfo); + fetchAllSystemData.then(setAllInfo).catch(() => console.warn('Error when fetching institution info')); }, []); const renderHierarchy = (data: InstitutionData | null): JSX.Element => { @@ -20,15 +21,15 @@ export function SystemConfigurationTool(): JSX.Element | null {
  • -

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

    +

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

    console.log(`'Add new division to institution' ${data.institution.id}`)} + onClick={() => console.log(`'Add new division to institution' ${data.id}`)} />
      - {data.institution.children.map((division) => ( + {data.children.map((division) => (
    • {`Division: ${division.name}`}

      @@ -75,27 +76,31 @@ export function SystemConfigurationTool(): JSX.Element | null {

      {userText.systemConfigurationTool()}

      - {allInfo === undefined || allInfo === null ? undefined : renderHierarchy(allInfo)} + {allInfo === undefined || allInfo === null ? : renderHierarchy(allInfo)}
      ); } type InstitutionData = { + // Institution readonly id: number; readonly name: string; - readonly children: readonly { + readonly children: RA<{ + // Division readonly id: number; readonly name: string; - readonly children: readonly { + readonly children: RA<{ + // Discipline readonly id: number; readonly name: string; - readonly children: readonly { + readonly children: RA<{ + // Collection readonly id: number; readonly name: string; - }[]; - }[]; - }[]; + }>; + }>; + }>; }; let institutionData: InstitutionData; From 3e11039edd4cafe2cb232cfe38bf825d54abaec1 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:07:48 -0800 Subject: [PATCH 5/6] Test new resource --- .../lib/components/FormParse/webOnlyViews.ts | 6 +++ .../components/Toolbar/SystemConfigTool.tsx | 43 ++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) 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/Toolbar/SystemConfigTool.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx index 217a988cad6..4ebc45062ab 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx @@ -1,15 +1,27 @@ 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 { LoadingScreen } from '../Molecules/Dialog'; +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')); }, []); @@ -29,7 +41,7 @@ export function SystemConfigurationTool(): JSX.Element | null { />
        - {data.children.map((division) => ( + {data.institution.children.map((division) => (
      • {`Division: ${division.name}`}

        @@ -48,7 +60,12 @@ export function SystemConfigurationTool(): JSX.Element | null { console.log(`'Add new collection to discipline' ${discipline.id}`)}/> + onClick={() =>{ + console.log(`'Add new collection to discipline' ${discipline.id}`) + setParentId(discipline.id) + setNewResource(new tables.Collection.Resource()) + handleNewResource() + }}/>
        {discipline.children.length > 0 && (
          @@ -78,12 +95,28 @@ export function SystemConfigurationTool(): JSX.Element | null {
          {allInfo === undefined || allInfo === null ? : renderHierarchy(allInfo)}
          + {newResourceOpen ? + + console.log('Click')} + /> + + : undefined} ); } type InstitutionData = { - // Institution + readonly 'institution': { + // Institution readonly id: number; readonly name: string; readonly children: RA<{ @@ -100,7 +133,7 @@ type InstitutionData = { readonly name: string; }>; }>; - }>; + }>;} }; let institutionData: InstitutionData; From f68b7a6bb3d04d05beb009adac9850098bb00ab7 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:07:27 -0800 Subject: [PATCH 6/6] Update backend return --- specifyweb/context/views.py | 4 +- .../components/Toolbar/SystemConfigTool.tsx | 40 ++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/specifyweb/context/views.py b/specifyweb/context/views.py index 4da902bbd16..5bb7075b665 100644 --- a/specifyweb/context/views.py +++ b/specifyweb/context/views.py @@ -694,14 +694,12 @@ def all_system_data(request): division_map[discipline.division_id]["children"].append(discipline_map[discipline.id]) institution_data = { - "institution": { "id": institution.id, "name": institution.name, "children": list(division_map.values()) - } } - return JsonResponse({"institution": institution_data}, safe=False) + 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/Toolbar/SystemConfigTool.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx index 4ebc45062ab..e8bf9029654 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx @@ -18,7 +18,7 @@ export function SystemConfigurationTool(): JSX.Element | null { const [newResourceOpen, handleNewResource, closeNewResource] = useBooleanState() - const [parentId, setParentId] = React.useState() + const [parentId, setParentId] = React.useState() const [newResource, setNewResource] = React.useState | undefined>() @@ -26,29 +26,39 @@ export function SystemConfigurationTool(): JSX.Element | null { fetchAllSystemData.then(setAllInfo).catch(() => console.warn('Error when fetching institution info')); }, []); - const renderHierarchy = (data: InstitutionData | null): JSX.Element => { - if (!data) return ; + const renderHierarchy = (institution: InstitutionData | null): JSX.Element => { + if (!institution) return ; return (
          • -

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

            +

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

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

              console.log(`'Add new discipline to division' ${division.id}`)} + onClick={() => { + console.log(`'Add new discipline to division' ${division.id}`) + setParentId(division.id) + setNewResource(new tables.Discipline.Resource()) + handleNewResource() + }} />
              {division.children.length > 0 && ( @@ -115,8 +125,10 @@ export function SystemConfigurationTool(): JSX.Element | null { } type InstitutionData = { - readonly 'institution': { - // Institution + /* + * Readonly 'institution': { + * // Institution + */ readonly id: number; readonly name: string; readonly children: RA<{ @@ -134,16 +146,16 @@ type InstitutionData = { }>; }>; }>;} -}; +// }; let institutionData: InstitutionData; -export const fetchAllSystemData = load<{ readonly institution: InstitutionData }>( +export const fetchAllSystemData = load( '/context/all_system_data.json', 'application/json' -).then((data: { readonly institution: InstitutionData }) => { - institutionData = data.institution; - return institutionData; +).then((data: InstitutionData) => { + institutionData = data; + return data; }); export const getAllInfo = ():InstitutionData => institutionData \ No newline at end of file