diff --git a/README.md b/README.md index 32d298ca9..e81655416 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,12 @@ Data Repository and Workflow Management for 3D data captures, models, and scenes ## Setup instructions (Development): -**Note**: *the setup instructions below are **out of date**. Updates are expected by 6/15/2022* - *Note: `.env.dev` is required and it follows `.env.template`* -### Development: - #### Prerequisites: -It is recommended to install [Volta](https://volta.sh/) which keeps node version in check. The versions can be specified in `package.json` and when switched to the directory of the project, volta automatically switches to the correct node version. +* It is recommended to install [Volta](https://volta.sh/) which keeps node version in check. The versions can be specified in `package.json` and when switched to the directory of the project, volta automatically switches to the correct node version. +* It is also recommended to use [Yarn](https://yarnpkg.com/) as the primary package manager for Packrat. + ``` cd ~ @@ -17,26 +15,61 @@ curl https://get.volta.sh | bash volta install node volta install yarn ``` -Now when you switch to the `dpo-packrat` repo, your node version would automatically pinned to the correct version by volta. +When switching to the `dpo-packrat` repo, the node version will automatically be pinned to the correct version by volta. - -1. Install the dependencies: +#### Steps: +1. Install the dependencies *(at the root level)*. [Lerna](https://lerna.js.org/) will ensure the subdirectories’ packages are also installed. ``` yarn ``` -2. Build the docker images, if they're already available then this would just start them (if you're on a mac then make sure Docker for mac is running): +2. Export the environment variables *(at the root level)* after `.env.dev` has been configured. +``` +export $(grep -v ‘^#’ .env.dev | xargs) +``` -``` +3. Generate GraphQL and Prisma code *(in the server directory)*. +*Note: make sure to generate them whenever changes have been made to the GraphQL schemas.* + +``` +cd server +yarn generate +``` + +4. If using Docker, build the Docker images and containers *(at the root)*. If they're already available then issuing the following command would start the containers. +*Note: if building the images and containers, this process can take an upwards of 20 minutes.* + +**If setting up Packrat without Docker, skip to step 9** + +``` +cd .. yarn dev ``` -3. Now the docker containers should start in 10s-20s. The client should be reachable at `http://localhost:3000` and server should be reachable at `http://localhost:4000` or the ports you specified in `.env.dev` following `.env.template` +5. Once the Docker images and containers are built, they should start in 10s-20s. The client should be reachable at `http://localhost:3000` and server should be reachable at `http://localhost:4000`, or the ports specified in `.env.dev`. + +6. To follow debug logs for `client` or `server` container, run `yarn log:client` or `yarn log:server` *(at the root)*. + +7. To log in and use Packrat, the database must be first initialized with data and user info *(in the server directory)*. + +``` +cd server +yarn initdbdock +``` + +8. After the database has been initialized, Solr enterprise-search needs to be indexed in order to populate the repository. +Navigate to `localhost:4000/solrindex` in the browser and wait for the result to appear. Once Solr finishes indexing, set up via Docker is complete! + +*Note: sometimes a failure message will appear even upon successful indexing. Successful indexing can be seen in the server container logs and by visiting the repository and finding the newly populated entries.* -4. If you want to follow debug logs for `client` or `server` container then just run `yarn log:client` or `yarn log:server` +9. If setting up Packrat without Docker, compile the typescript code in a separate terminal *(for the common directory)*. +``` +cd common +yarn start +``` -5. If not using docker run each command in a separate terminal for the package you're developing: +10. Run each command in separate terminals *(at the root level)*: **For client:** @@ -50,10 +83,16 @@ yarn start:client yarn start:server ``` +11. Setting up Packrat without Docker gives users the flexbility to install and configure their own database and Solr caching as needed. + + +**Congratulations, Packrat is now ready for use!** + + # Alternative docker workflow: ``` -# If step 2. wasn't working, follow this alternative instead. +# If step 4 for building and starting the docker containers are failing, follow this alternative instead. # Creates Devbox for packrat yarn devbox:up # Creates DB for devbox @@ -107,20 +146,3 @@ sudo mount /tmp -o remount,exec 5. Wait for the images to be build/updated, then use `./conf/scripts/cleanup.sh` script to cleanup any residual docker images are left (optional) 6. Make sure nginx is active using `sudo service nginx status --no-pager` - -## Start databases (Production server only): - -1. Start `dev` or `prod` databases using `./conf/scripts/initdb.sh` script -``` -MYSQL_ROOT_PASSWORD= ./conf/scripts/initdb.sh dev -``` -*Note: `MYSQL_ROOT_PASSWORD` be same what you mentioned in the `.env.dev` or `.env.prod` file for that particular environment. Mostly would be used for `dev` environment.* - -## Update production nginx configuration (Production server only): - -1. Make the changes to production nginx configuration is located at `scripts/proxy/nginx.conf` - -2. Use `conf/scripts/refreshProxy.sh` script to restart/update nginx service -``` -./conf/scripts/refreshProxy.sh -``` diff --git a/client/src/pages/Admin/components/AdminUsersView.tsx b/client/src/pages/Admin/components/AdminUsersView.tsx index b711b022b..ebc942de1 100644 --- a/client/src/pages/Admin/components/AdminUsersView.tsx +++ b/client/src/pages/Admin/components/AdminUsersView.tsx @@ -49,7 +49,8 @@ function AdminUsersView(): React.ReactElement { active: User_Status.EAll, search: '' } - } + }, + fetchPolicy: 'no-cache' }); const { data: { @@ -70,7 +71,8 @@ function AdminUsersView(): React.ReactElement { active: newActive, search: newSearchText } - } + }, + fetchPolicy: 'no-cache' }); const { data: { diff --git a/client/src/pages/Ingestion/components/Metadata/Photogrammetry/AssetContents.tsx b/client/src/pages/Ingestion/components/Metadata/Photogrammetry/AssetContents.tsx index 163362ea4..fd8495ea8 100644 --- a/client/src/pages/Ingestion/components/Metadata/Photogrammetry/AssetContents.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Photogrammetry/AssetContents.tsx @@ -12,6 +12,7 @@ import { StateFolder, VocabularyOption } from '../../../../../store'; import { palette } from '../../../../../theme'; import { ViewableProps } from '../../../../../types/repository'; import { getNullableSelectEntries } from '../../../../../utils/controls'; +import { updatedFieldStyling } from '../../../../Repository/components/DetailsView/DetailsTab/CaptureDataDetails'; export const useStyles = makeStyles(({ palette, typography, breakpoints }) => ({ emptyFolders: { @@ -47,12 +48,13 @@ export const useStyles = makeStyles(({ palette, typography, breakpoints }) => ({ interface AssetContentsProps extends ViewableProps { folders: StateFolder[]; + originalFolders: StateFolder[]; options: VocabularyOption[]; onUpdate: (id: number, variantType: number) => void; } function AssetContents(props: AssetContentsProps): React.ReactElement { - const { folders, options, onUpdate, disabled = false } = props; + const { folders, options, onUpdate, disabled = false, originalFolders } = props; const classes = useStyles(); return ( @@ -72,7 +74,7 @@ function AssetContents(props: AssetContentsProps): React.ReactElement { {folders.length > 0 && (folders.map(({ id, name, variantType }: StateFolder, index: number) => { const update = ({ target }) => onUpdate(id, target.value); - + const originalFolder = originalFolders.find((folder) => folder.name === name); return ( @@ -92,6 +94,7 @@ function AssetContents(props: AssetContentsProps): React.ReactElement { disableUnderline className={classes.select} SelectDisplayProps={{ style: { paddingLeft: '10px', paddingRight: '10px', borderRadius: '5px' } }} + style={{ ...updatedFieldStyling(originalFolder?.variantType !== variantType) }} > {getNullableSelectEntries(options, 'idVocabulary', 'Term').map(({ value, label }, index) => {label})} diff --git a/client/src/pages/Ingestion/components/Metadata/Photogrammetry/index.tsx b/client/src/pages/Ingestion/components/Metadata/Photogrammetry/index.tsx index 9237b31a5..0e4487d10 100644 --- a/client/src/pages/Ingestion/components/Metadata/Photogrammetry/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Photogrammetry/index.tsx @@ -251,6 +251,7 @@ function Photogrammetry(props: PhotogrammetryProps): React.ReactElement { diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/CaptureDataDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/CaptureDataDetails.tsx index 01b7096cd..f15381b36 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/CaptureDataDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/CaptureDataDetails.tsx @@ -9,7 +9,7 @@ import { Box, MenuItem, Select, Typography, Table, TableBody, TableCell, TableContainer, TableRow, Paper, Checkbox } from '@material-ui/core'; import React, { useEffect } from 'react'; import { DateInputField, Loader } from '../../../../../components'; -import { parseFoldersToState, useVocabularyStore } from '../../../../../store'; +import { parseFoldersToState, StateFolder, useVocabularyStore } from '../../../../../store'; import { eVocabularySetID, eSystemObjectType } from '@dpo-packrat/common'; import { isFieldUpdated } from '../../../../../utils/repository'; import { withDefaultValueNumber } from '../../../../../utils/shared'; @@ -166,6 +166,10 @@ function CaptureDataDetails(props: DetailComponentProps): React.ReactElement { const cdMethods = getEntries(eVocabularySetID.eCaptureDataCaptureMethod); const captureMethodidVocabulary = withDefaultValueNumber(CaptureDataDetails?.captureMethod, getInitialEntry(eVocabularySetID.eCaptureDataCaptureMethod)); const captureMethod = cdMethods.find(method => method.idVocabulary === captureMethodidVocabulary); + + const cdDetailsDate = new Date(CaptureDataDetails.dateCaptured as string); + const cdDataDate = new Date(captureDataData?.dateCaptured as string); + return ( setDateField('dateCaptured', value)} @@ -235,6 +239,7 @@ function CaptureDataDetails(props: DetailComponentProps): React.ReactElement { folders={parseFoldersToState(CaptureDataDetails?.folders ?? [])} options={getEntries(eVocabularySetID.eCaptureDataFileVariantType)} onUpdate={updateFolderVariant} + originalFolders={data?.getDetailsTabDataForObject?.CaptureData?.folders as StateFolder[]} /> diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx index 7dd110f30..6cc85c059 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx @@ -18,7 +18,8 @@ export interface ItemDetailFields extends SubjectDetailFields { } function ItemDetails(props: DetailComponentProps): React.ReactElement { - const { data, loading, disabled, onUpdateDetail, objectType, subtitle, onSubtitleUpdate } = props; + const { data, loading, disabled, onUpdateDetail, objectType, subtitle, onSubtitleUpdate, originalSubtitle } = props; + const [ItemDetails, updateDetailField] = useDetailTabStore(state => [state.ItemDetails, state.updateDetailField]); useEffect(() => { @@ -50,6 +51,7 @@ function ItemDetails(props: DetailComponentProps): React.ReactElement { disabled={disabled} onChange={onSetField} subtitle={subtitle} + originalSubtitle={originalSubtitle} onSubtitleUpdate={onSubtitleUpdate} isItemView setCheckboxField={setCheckboxField} diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx index ad2925097..76f4135b6 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx @@ -16,6 +16,8 @@ import { DetailComponentProps } from './index'; import { useStyles as useSelectStyles, SelectFieldProps } from '../../../../../components/controls/SelectField'; import { DebounceInput } from 'react-debounce-input'; import ObjectMeshTable from '../../../../Ingestion/components/Metadata/Model/ObjectMeshTable'; +import { updatedFieldStyling } from './CaptureDataDetails'; +import { isFieldUpdated } from '../../../../../utils/repository'; export const useStyles = makeStyles(({ palette, typography }) => ({ notRequiredFields: { @@ -85,7 +87,7 @@ export const useStyles = makeStyles(({ palette, typography }) => ({ function ModelDetails(props: DetailComponentProps): React.ReactElement { const classes = useStyles(); - const { data, loading, onUpdateDetail, objectType, subtitle, onSubtitleUpdate } = props; + const { data, loading, onUpdateDetail, objectType, subtitle, onSubtitleUpdate, originalSubtitle } = props; const { ingestionModel, modelObjects } = extractModelConstellation(data?.getDetailsTabDataForObject?.Model); const [ModelDetails, updateDetailField] = useDetailTabStore(state => [state.ModelDetails, state.updateDetailField]); @@ -132,7 +134,7 @@ function ModelDetails(props: DetailComponentProps): React.ReactElement {
- +
@@ -141,8 +143,8 @@ function ModelDetails(props: DetailComponentProps): React.ReactElement { Date Created
-
- setDateField(date)} dateHeight='22px' /> +
+ setDateField(date)} dateHeight='22px' updated={new Date(ingestionModel.DateCreated).toString() !== new Date(ModelDetails?.DateCreated as string)?.toString()} />
@@ -235,7 +242,7 @@ export default ModelDetails; function SelectField(props: SelectFieldProps): React.ReactElement { - const { value, name, options, onChange, disabled = false, label } = props; + const { value, name, options, onChange, disabled = false, label, updated = false } = props; const classes = useSelectStyles(props); return ( @@ -253,7 +260,7 @@ function SelectField(props: SelectFieldProps): React.ReactElement { disabled={disabled} disableUnderline inputProps={{ 'title': `${name} select`, style: { width: '100%' } }} - style={{ minWidth: '100%', width: 'fit-content' }} + style={{ minWidth: '100%', width: 'fit-content', ...updatedFieldStyling(updated) }} SelectDisplayProps={{ style: { paddingLeft: '10px', borderRadius: '5px' } }} > {options.map(({ idVocabulary, Term }, index) => ( diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/SceneDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/SceneDetails.tsx index a6c24f667..989c29401 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/SceneDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/SceneDetails.tsx @@ -13,6 +13,7 @@ import { apolloClient } from '../../../../../graphql/index'; import { ReadOnlyRow, CheckboxField, InputField } from '../../../../../components/index'; import { useDetailTabStore } from '../../../../../store'; import { eSystemObjectType } from '@dpo-packrat/common'; +import { isFieldUpdated } from '../../../../../utils/repository'; export const useStyles = makeStyles(({ palette }) => ({ value: { @@ -36,9 +37,9 @@ export const useStyles = makeStyles(({ palette }) => ({ function SceneDetails(props: DetailComponentProps): React.ReactElement { const classes = useStyles(); const isMounted = useRef(false); - const { data, loading, onUpdateDetail, objectType, subtitle, onSubtitleUpdate } = props; + const { data, loading, onUpdateDetail, objectType, subtitle, onSubtitleUpdate, originalSubtitle } = props; const [SceneDetails, updateDetailField] = useDetailTabStore(state => [state.SceneDetails, state.updateDetailField]); - + const sceneData = data?.getDetailsTabDataForObject.Scene; useEffect(() => { const retrieveSceneData = async () => { if (data && !loading) { @@ -100,6 +101,7 @@ function SceneDetails(props: DetailComponentProps): React.ReactElement { required padding='3px 10px 1px 10px' containerStyle={{ borderTopLeftRadius: '5px', borderTopRightRadius: '5px', paddingTop: '4px' }} + updated={subtitle !== originalSubtitle} /> ) => void; } @@ -75,6 +76,7 @@ export function SubjectFields(props: SubjectFieldsProps): React.ReactElement { ItemDetails, itemData, subtitle, + originalSubtitle, onSubtitleUpdate } = props; const classes = useStyles(); @@ -119,7 +121,7 @@ export function SubjectFields(props: SubjectFieldsProps): React.ReactElement { padding: '0px 10px', borderRadius: 5, border: '1px solid rgba(141, 171, 196, 0.4)', - ...updatedFieldStyling(isFieldUpdated(details, originalFields, 'Subtitle')) + ...updatedFieldStyling(subtitle !== originalSubtitle) }} />
diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/index.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/index.tsx index a75cbf5b0..d0a7e2d31 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/index.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/index.tsx @@ -83,6 +83,7 @@ export interface DetailComponentProps extends GetDetailsTabDataForObjectQueryRes disabled: boolean; objectType: number; subtitle: string; + originalSubtitle: string; onSubtitleUpdate: (e) => void; onUpdateDetail: (objectType: number, data: UpdateDataFields) => void; } @@ -113,6 +114,7 @@ type DetailsTabParams = { onUpdateDetail: (objectType: number, data: UpdateDataFields) => void; onSubtitleUpdate: (e) => void; subtitle?: string; + originalSubtitle?: string; objectVersions: SystemObjectVersion[]; detailQuery: any; metadata: Metadata[] @@ -131,6 +133,7 @@ function DetailsTab(props: DetailsTabParams): React.ReactElement { onUpdateDetail, onSubtitleUpdate, subtitle, + originalSubtitle, objectVersions, detailQuery, metadata @@ -214,7 +217,8 @@ function DetailsTab(props: DetailsTabParams): React.ReactElement { objectType, disabled, onSubtitleUpdate, - subtitle + subtitle, + originalSubtitle }; const detailsProps = { diff --git a/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx b/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx index e2e2b99f6..b81b50390 100644 --- a/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx @@ -4,7 +4,7 @@ * * This component renders object details for the Repository Details UI. */ -import { Box, Checkbox, Typography, Select, MenuItem } from '@material-ui/core'; +import { Box, Checkbox, Typography, Select, MenuItem, Tooltip } from '@material-ui/core'; import { withStyles, makeStyles } from '@material-ui/core/styles'; import React, { useEffect, useState } from 'react'; import { NewTabLink } from '../../../../components'; @@ -17,6 +17,8 @@ import { getTermForSystemObjectType } from '../../../../utils/repository'; import { LoadingButton } from '../../../../components'; import { toast } from 'react-toastify'; import { eSystemObjectType, ePublishedState } from '@dpo-packrat/common'; +import { ToolTip } from '../../../../components'; +import { HelpOutline } from '@material-ui/icons'; const useStyles = makeStyles(({ typography, palette }) => ({ detail: { @@ -135,6 +137,7 @@ function ObjectDetails(props: ObjectDetailsProps): React.ReactElement { } = props; const [licenseList, setLicenseList] = useState([]); const [loading, setLoading] = useState(false); + const [isTooltipOpen, setIsTooltipOpen] = useState(false); const isRetiredUpdated: boolean = isFieldUpdated({ retired }, originalFields, 'retired'); const getEntries = useLicenseStore(state => state.getEntries); const classes = useObjectDetailsStyles(props); @@ -232,6 +235,7 @@ function ObjectDetails(props: ObjectDetailsProps): React.ReactElement {  Publish  API Only  {(publishedEnum !== ePublishedState.eNotPublished) && (Unpublish)} +  }> setIsTooltipOpen(!isTooltipOpen)} onMouseLeave={() => setIsTooltipOpen(false)} style={{ alignSelf: 'center', cursor: 'pointer' }} />
} /> @@ -349,3 +353,15 @@ function Detail(props: DetailProps): React.ReactElement { } export default ObjectDetails; + +const scenePublishNotes = +`In order to publish a scene to EDAN, the following criteria must be met: +-The scene must have thumbnails. +-The scene must be Posed and QC'd (and marked as such on the Details tab). +-The scene must be Approved for Publishing (and marked as such on the Details tab). +-The license controlling the scene must allow for publishing (i.e. not "None" and not "Restricted"). +Clicking "Publish" transmits the scene package to EDAN and marks the EDAN record as searchable. Scene downloads will be sent, too, if the license allows it. +Clicking "API Only" transmits the scene package to EDAN, but marks the EDAN record as not searchable. As well, scene downloads are sent if allowed by the license. +For published scenes, clicking "Unpublish" marks the scene package as inactive and not searchable. +Changes made to scenes are only published to EDAN when the user makes use of "Publish", "API Only", or "Unpublish". +Users must explicitly publish these changes to EDAN.`; \ No newline at end of file diff --git a/client/src/pages/Repository/components/DetailsView/index.tsx b/client/src/pages/Repository/components/DetailsView/index.tsx index 6f098e976..3e1f01fc3 100644 --- a/client/src/pages/Repository/components/DetailsView/index.tsx +++ b/client/src/pages/Repository/components/DetailsView/index.tsx @@ -136,20 +136,22 @@ function DetailsView(): React.ReactElement { const [getAllMetadataEntries, areMetadataUpdated, metadataControl, metadataDisplay, validateMetadataFields, initializeMetadata, resetMetadata] = useObjectMetadataStore(state => [state.getAllMetadataEntries, state.areMetadataUpdated, state.metadataControl, state.metadataDisplay, state.validateMetadataFields, state.initializeMetadata, state.resetMetadata]); const objectDetailsData = data; + const fetchDetailTabDataAndSetState = async () => { + const detailsTabData = await getDetailsTabDataForObject(idSystemObject, objectType); + setDetailQuery(detailsTabData); + initializeDetailFields(detailsTabData, objectType); + if (objectType === eSystemObjectType.eSubject) { + initializePreferredIdentifier(detailsTabData?.data?.getDetailsTabDataForObject?.Subject?.idIdentifierPreferred); + setLoadingIdentifiers(false); + } + }; useEffect(() => { if (data) { - const fetchDetailTabDataAndInitializeStateStore = async () => { - const detailsTabData = await getDetailsTabDataForObject(idSystemObject, objectType); - setDetailQuery(detailsTabData); - initializeDetailFields(detailsTabData, objectType); - if (objectType === eSystemObjectType.eSubject) { - initializePreferredIdentifier(detailsTabData?.data?.getDetailsTabDataForObject?.Subject?.idIdentifierPreferred); - setLoadingIdentifiers(false); - } + const handleDetailTab = async () => { + await fetchDetailTabDataAndSetState(); }; - - fetchDetailTabDataAndInitializeStateStore(); + handleDetailTab(); } }, [idSystemObject, data]); @@ -485,6 +487,7 @@ function DetailsView(): React.ReactElement { if (data?.updateObjectDetails?.success) { const message: string | null | undefined = data?.updateObjectDetails?.message; toast.success(`Data saved successfully${message? ': ' + message : ''}`); + fetchDetailTabDataAndSetState(); return true; } else throw new Error(data?.updateObjectDetails?.message ?? ''); @@ -570,6 +573,7 @@ function DetailsView(): React.ReactElement { onUpdateDetail={onUpdateDetail} onSubtitleUpdate={onSubtitleUpdate} subtitle={details?.subtitle} + originalSubtitle={data.getSystemObjectDetails?.subTitle || ''} objectVersions={objectVersions} detailQuery={detailQuery} metadata={metadata} diff --git a/client/src/store/item.ts b/client/src/store/item.ts index 4e2aea27d..04a24d9a4 100644 --- a/client/src/store/item.ts +++ b/client/src/store/item.ts @@ -55,7 +55,7 @@ type ItemStore = { updateNewItemProject: (idProject: number) => void; updateSelectedItem: (id: string) => void; loadingItems: () => void; - fetchIngestionItems: (idSubjects: number[]) => Promise; + fetchAndInitializeIngestionItems: (idSubjects: number[]) => Promise; reset: () => void; }; @@ -117,7 +117,6 @@ export const useItemStore = create((set: SetState, get: Ge newItemCopy.selected = true; set({ hasNewItem: true, items: itemsCopy, newItem: newItemCopy }); }, - updateNewItemEntireSubject: (entireSubject: boolean) => { const { newItem } = get(); const newItemCopy = lodash.cloneDeep(newItem); @@ -159,7 +158,8 @@ export const useItemStore = create((set: SetState, get: Ge loadingItems: (): void => { set({ loading: true }); }, - fetchIngestionItems: async (idSubjects: number[]): Promise => { + fetchAndInitializeIngestionItems: async (idSubjects: number[]): Promise => { + const { hasNewItem } = get(); try { const ingestionItemQuery: ApolloQueryResult = await apolloClient.query({ query: GetIngestionItemsDocument, @@ -172,6 +172,7 @@ export const useItemStore = create((set: SetState, get: Ge }); const { data: { getIngestionItems: { IngestionItem } } } = ingestionItemQuery; const ingestionItemState = IngestionItem?.map(item => parseIngestionItemToState(item)); + if (ingestionItemState?.length === 1 && !hasNewItem) ingestionItemState[0].selected = true; set({ items: ingestionItemState }); } catch (error) { toast.error('Failed to get media group for subjects'); diff --git a/client/src/store/subject.ts b/client/src/store/subject.ts index d015c579e..421101eb5 100644 --- a/client/src/store/subject.ts +++ b/client/src/store/subject.ts @@ -52,7 +52,7 @@ export const useSubjectStore = create((set: SetState updateProjectsAndItemsForSubjects(selectedSubjects); }, updateProjectsAndItemsForSubjects: async (selectedSubjects: StateSubject[]): Promise => { - const { addItems, fetchIngestionItems, updateNewItemEntireSubject } = useItemStore.getState(); + const { addItems, fetchAndInitializeIngestionItems, updateNewItemEntireSubject } = useItemStore.getState(); if (!selectedSubjects.length) { addItems([]); @@ -63,7 +63,7 @@ export const useSubjectStore = create((set: SetState updateNewItemEntireSubject(false); try { - await fetchIngestionItems(selectedSubjects.map(subject => subject.id)); + await fetchAndInitializeIngestionItems(selectedSubjects.map(subject => subject.id)); } catch (error) { toast.error('Failed to get ingestion items'); } diff --git a/client/src/utils/repository.tsx b/client/src/utils/repository.tsx index c1b56b7e6..00de99082 100644 --- a/client/src/utils/repository.tsx +++ b/client/src/utils/repository.tsx @@ -115,6 +115,18 @@ export function parseRepositoryUrl(search: string): any { const dateCreatedTo: Date = convertLocalDateToUTC(new Date(dateString)); filter.dateCreatedTo = safeDate(dateCreatedTo); } + + const searchS: RegExpMatchArray | null = search.match(/search=(.*?)([&]|$)/); + if (searchS && searchS.length >= 2) { + const searchString: string = decodeURIComponent(searchS[1]); + filter.search = searchString; + } + + const keywordS: RegExpMatchArray | null = search.match(/keyword=(.*?)([&]|$)/); + if (keywordS && keywordS.length >= 2) { + const keywordString: string = decodeURIComponent(keywordS[1]); + filter.keyword = keywordString; + } return filter; } diff --git a/docs/content/user/search/_index.md b/docs/content/user/search/_index.md index 938992a9d..7b9d5783c 100644 --- a/docs/content/user/search/_index.md +++ b/docs/content/user/search/_index.md @@ -10,7 +10,8 @@ When searching, the display of repository contents is updated with objects that Search tips: * Packrat's keyword search uses the [Extended DisMax Query Parser](https://solr.apache.org/guide/8_11/the-extended-dismax-query-parser.html) -* Surround text with double quotes to require that exact text to be matched +* Prefix text with "+" to require that exact text to be matched +* Surround multiple words with double quotes to search for that phrase * Use * and ? for wildcard searches * Packrat will search for objects with matching identifiers, such as ARK IDs and EDAN MDM IDs * Capitalization is ignored diff --git a/server/collections/impl/EdanCollection.ts b/server/collections/impl/EdanCollection.ts index e0640e80e..bfd0d8dfa 100644 --- a/server/collections/impl/EdanCollection.ts +++ b/server/collections/impl/EdanCollection.ts @@ -120,6 +120,32 @@ export class EdanCollection implements COL.ICollection { return result; } + async fetchContent(id?: string, url?: string): Promise { + LOG.info(`EdanCollection.fetchContent(${id}, ${url})`, LOG.LS.eCOLL); + let params: string = ''; + if (id) + params = `id=${encodeURIComponent(id)}`; + else if (url) + params = `url=${encodeURIComponent(url)}`; + else + return null; + + const reqResult: HttpRequestResult = await this.sendRequest(eAPIType.eEDAN, eHTTPMethod.eGet, 'content/v2.0/content/getContent.htm', params); + if (!reqResult.success) { + LOG.error(`EdanCollection.fetchContent(${id}, ${url})`, LOG.LS.eCOLL); + return null; + } + + let jsonResult: any | null = null; + try { + jsonResult = reqResult.output ? JSON.parse(reqResult.output) : /* istanbul ignore next */ null; + } catch (error) /* istanbul ignore next */ { + LOG.error(`EdanCollection.fetchContent(${id}, ${url})`, LOG.LS.eCOLL, error); + return null; + } + return jsonResult; + } + async publish(idSystemObject: number, ePublishState: number): Promise { switch (ePublishState) { case COMMON.ePublishedState.eNotPublished: diff --git a/server/collections/impl/PublishScene.ts b/server/collections/impl/PublishScene.ts index 81fb62a88..0158f3560 100644 --- a/server/collections/impl/PublishScene.ts +++ b/server/collections/impl/PublishScene.ts @@ -528,6 +528,43 @@ export class PublishScene { return true; } + static async mapEdanUnitsToPackratVocabulary(units: COL.Edan3DResourceAttributeUnits): Promise { + let eVocabID: COMMON.eVocabularyID | undefined = undefined; + switch (units) { + case 'mm': eVocabID = COMMON.eVocabularyID.eEdan3DResourceAttributeUnitsmm; break; + case 'cm': eVocabID = COMMON.eVocabularyID.eEdan3DResourceAttributeUnitscm; break; + case 'm': eVocabID = COMMON.eVocabularyID.eEdan3DResourceAttributeUnitsm; break; + case 'km': eVocabID = COMMON.eVocabularyID.eEdan3DResourceAttributeUnitskm; break; + case 'in': eVocabID = COMMON.eVocabularyID.eEdan3DResourceAttributeUnitsin; break; + case 'ft': eVocabID = COMMON.eVocabularyID.eEdan3DResourceAttributeUnitsft; break; + case 'yd': eVocabID = COMMON.eVocabularyID.eEdan3DResourceAttributeUnitsyd; break; + case 'mi': eVocabID = COMMON.eVocabularyID.eEdan3DResourceAttributeUnitsmi; break; + } + return eVocabID ? await CACHE.VocabularyCache.vocabularyByEnum(eVocabID) : undefined; + } + + static computeDownloadType(category?: COL.Edan3DResourceCategory | undefined, + MODEL_FILE_TYPE?: COL.Edan3DResourceAttributeModelFileType | undefined, DRACO_COMPRESSED?: boolean | undefined): string { + + let tag: string = ''; + switch (category) { + case 'Low resolution': + switch (MODEL_FILE_TYPE) { + case 'glb': tag = DRACO_COMPRESSED ? 'webassetglbarcompressed' : 'webassetglblowuncompressed'; break; + case 'obj': tag = 'objziplow'; break; + case 'gltf': tag = 'gltfziplow'; break; + } + break; + case 'iOS AR model': + tag = (MODEL_FILE_TYPE === 'usdz') ? 'usdz' : ''; + break; + case 'Full resolution': + tag = (MODEL_FILE_TYPE === 'obj') ? 'objzipfull' : ''; + break; + } + return tag; + } + private async extractResource(SAC: SceneAssetCollector, uuid: string): Promise { let type: COL.Edan3DResourceType | undefined = undefined; let UNITS: COL.Edan3DResourceAttributeUnits | undefined = undefined; diff --git a/server/collections/interface/ICollection.ts b/server/collections/interface/ICollection.ts index c067e7124..e02edd568 100644 --- a/server/collections/interface/ICollection.ts +++ b/server/collections/interface/ICollection.ts @@ -45,6 +45,7 @@ export type CollectionQueryOptions = { */ export interface ICollection { queryCollection(query: string, rows: number, start: number, options: CollectionQueryOptions | null): Promise; + fetchContent(id?: string, url?: string): Promise; publish(idSystemObject: number, ePublishState: number): Promise; createEdanMDM(edanmdm: EdanMDMContent, status: number, publicSearch: boolean): Promise; createEdan3DPackage(path: string, sceneFile?: string | undefined): Promise; diff --git a/server/config/solr/data/packrat/conf/schema.xml b/server/config/solr/data/packrat/conf/schema.xml index 3e345421a..5fd7d955c 100644 --- a/server/config/solr/data/packrat/conf/schema.xml +++ b/server/config/solr/data/packrat/conf/schema.xml @@ -181,6 +181,7 @@ +