├── .babelrc ├── .circleci └── config.yml ├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── ISSUE_TEMPLATE.md │ ├── bug_report.md │ ├── feature_request.md │ └── sinopia-alma-integration-request.md └── pull_request_template.md ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── Dockerfile ├── Dockerfile.cypress ├── Dockerfile.proxy ├── LICENSE ├── README.md ├── __mocks__ ├── fileMock.js ├── mockAWSData.js └── styleMock.js ├── __tests__ ├── Config.test.js ├── GraphBuilder.test.js ├── ResourceDiffer.test.js ├── TemplatesBuilder.test.js ├── __action_fixtures__ │ ├── addSiblingValueSubject-ADD_VALUE.js │ ├── expandProperty-ADD_PROPERTY.js │ ├── expandProperty-ADD_VALUE.js │ ├── loadResource-ADD_SUBJECT-multiple-property-uris.js │ ├── loadResource-ADD_SUBJECT.js │ ├── newResource-ADD_SUBJECT.js │ ├── newResourceCopy-ADD_SUBJECT.js │ ├── newResourceFromDataset-ADD_SUBJECT-bad-ordered.js │ ├── newResourceFromDataset-ADD_SUBJECT-nested.js │ ├── newResourceFromDataset-ADD_SUBJECT-ordered.js │ ├── newResourceFromDataset-ADD_SUBJECT.js │ ├── subjectTemplate-inputs.js │ ├── subjectTemplate-literal.js │ ├── subjectTemplate-multiple_property_uris.js │ ├── subjectTemplate-ordered.js │ ├── subjectTemplate-suppressible.js │ └── subjectTemplate-uri.js ├── __resource_fixtures__ │ ├── instance.json │ ├── instance_with_refs.json │ ├── invalid_instance.json │ ├── invalid_rt.json │ ├── test-inputs.json │ ├── test-multiple_property_uris.json │ ├── test.json │ └── test2.json ├── __state_fixtures__ │ ├── full_resource.json │ └── value_subject.json ├── __template_fixtures__ │ ├── DiscogsLookup.json │ ├── Immutable.json │ ├── Instance.json │ ├── Note.json │ ├── SinopiaLookup.json │ ├── Title.json │ ├── TitleNote.json │ ├── TitleNoteOptionalClass.json │ ├── WikidataLookup.json │ ├── Work.json │ ├── WorkTitle.json │ ├── ld4p_RT_bf2_Form.json │ ├── ld4p_RT_bf2_RareMat_RBMS.json │ ├── ld4p_RT_bf2_Title_AbbrTitle.json │ ├── nonUniqueValueTemplateRefs.json │ ├── notFoundValueTemplateRefs.json │ ├── propertyURIRepeated.json │ ├── testing_dupe_properties.json │ ├── testing_inputs.json │ ├── testing_literal.json │ ├── testing_literalValidation.json │ ├── testing_lookups.json │ ├── testing_multiple_class_inputs.json │ ├── testing_multiple_class_literal.json │ ├── testing_multiple_property_uris.json │ ├── testing_ordered.json │ ├── testing_suppress_language.json │ ├── testing_suppressed_uri.json │ ├── testing_suppressible.json │ ├── testing_uri.json │ ├── uber_template1.json │ ├── uber_template2.json │ ├── uber_template3.json │ ├── uber_template4.json │ └── uber_template5.json ├── actionCreators │ ├── authenticate.test.js │ ├── exports.test.js │ ├── history.test.js │ ├── languages.test.js │ ├── lookups.test.js │ ├── relationships.test.js │ ├── resources.loadResource.test.js │ ├── resources.newResource.test.js │ ├── resources.newResourceCopy.test.js │ ├── resources.newResourceFromDataset.test.js │ ├── resources.test.js │ ├── search.test.js │ ├── templateValidationHelpers.test.js │ ├── templates.test.js │ ├── transfer.test.js │ └── user.test.js ├── components │ ├── App.test.js │ ├── LongDate.test.js │ ├── editor │ │ ├── RDFDisplay.test.js │ │ ├── inputs │ │ │ └── RenderLookupContext.test.js │ │ ├── leftNav │ │ │ └── DiffDisplay.test.js │ │ ├── preview │ │ │ └── ResourcePreviewHeader.test.js │ │ └── property │ │ │ ├── LiteralTypeLabel.test.js │ │ │ ├── PropertyLabelInfo.test.js │ │ │ └── PropertyLabelInfoTooltip.test.js │ ├── home │ │ └── UserNotifications.test.js │ ├── search │ │ ├── GroupFilter.test.js │ │ ├── QASearchResults.test.js │ │ ├── Search.test.js │ │ ├── SearchResultsPaging.test.js │ │ ├── SinopiaSearchResults.test.js │ │ ├── SinopiaSort.test.js │ │ └── TypeFilter.test.js │ └── vocabulary │ │ └── Vocab.test.js ├── feature │ ├── dashboard.test.js │ ├── editing │ │ ├── addRemove.test.js │ │ ├── changeGroups.test.js │ │ ├── copyTemplateResource.test.js │ │ ├── editingClass.test.js │ │ ├── editingImmutable.test.js │ │ ├── editingLanguage.test.js │ │ ├── editingList.test.js │ │ ├── editingLiteral.test.js │ │ ├── editingLookup.test.js │ │ ├── editingPropertyUri.test.js │ │ ├── editingUri.test.js │ │ ├── editorRouting.test.js │ │ ├── expandContract.test.js │ │ ├── openAndCloseResource.test.js │ │ ├── propInfo.test.js │ │ ├── reordering.test.js │ │ ├── requestMARC.test.js │ │ ├── saveAndCopyResource.test.js │ │ ├── saveResource.test.js │ │ ├── transfer.test.js │ │ ├── versionsDiff.test.js │ │ ├── viewGroups.test.js │ │ └── viewRelationships.test.js │ ├── editorPreview.test.js │ ├── exportFile.test.js │ ├── invalidTemplate.test.js │ ├── loadNewResource.test.js │ ├── loadRDF.test.js │ ├── loadResource.test.js │ ├── metrics │ │ ├── viewResourceMetrics.test.js │ │ ├── viewTemplateMetrics.test.js │ │ └── viewUserMetrics.test.js │ ├── newResourceTemplate.test.js │ ├── readNews.test.js │ ├── search.test.js │ ├── searchAndOpenTemplate.test.js │ ├── searchAndPreviewResource.test.js │ ├── searchAndViewRelationships.test.js │ ├── unusedRDF.test.js │ ├── userAuthentication.test.js │ └── userPermissions.test.js ├── matchers │ ├── actions.js │ ├── matcherUtils.js │ ├── resources.js │ └── templates.js ├── reducers │ ├── authenticate.test.js │ ├── errors.test.js │ ├── exports.test.js │ ├── history.test.js │ ├── index.test.js │ ├── languages.test.js │ ├── lookups.test.js │ ├── messages.test.js │ ├── modals.test.js │ ├── relationships.test.js │ ├── resourceValidationHelpers.test.js │ ├── resources.test.js │ ├── search.test.js │ └── templates.test.js ├── selectors │ ├── authenticate.test.js │ ├── errors.test.js │ ├── languages.test.js │ ├── relationships.test.js │ ├── resources.test.js │ ├── search.test.js │ └── templates.test.js ├── sinopiaApi.test.js ├── sinopiaMetrics.test.js ├── sinopiaSearch.test.js ├── testUtilities │ ├── actionUtils.js │ ├── featureUtils.js │ ├── fixtureLoaderHelper.js │ ├── resolver.js │ ├── resourceBuilderUtils.js │ ├── stateResourceBuilderUtils.js │ ├── stateUtils.js │ └── testUtils.js └── utilities │ ├── Language.test.js │ ├── Lookup.test.js │ ├── QuestioningAuthority.test.js │ └── Utilities.test.js ├── cypress.json ├── cypress ├── fixtures │ ├── WorkTitle.txt │ ├── uber_template1.txt │ ├── uber_template2.txt │ ├── uber_template3.txt │ └── uber_template4.txt ├── integration │ ├── end2end.spec.js │ ├── helpAndResources.spec.js │ └── leftNav.spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── docker-compose.yml ├── docker └── proxy-httpd.conf ├── index.html ├── package-lock.json ├── package.json ├── react-testing-library.setup.js ├── redux-state.svg ├── release_process.md ├── server.js ├── src ├── Config.js ├── GraphBuilder.js ├── Honeybadger.js ├── ResourceDiffer.js ├── TemplatesBuilder.js ├── actionCreators │ ├── authenticate.js │ ├── exports.js │ ├── groups.js │ ├── history.js │ ├── languages.js │ ├── lookups.js │ ├── relationships.js │ ├── resourceHelpers.js │ ├── resources.js │ ├── search.js │ ├── templateValidationHelpers.js │ ├── templates.js │ ├── transfer.js │ └── user.js ├── actions │ ├── authenticate.js │ ├── errors.js │ ├── exports.js │ ├── groups.js │ ├── history.js │ ├── index.js │ ├── languages.js │ ├── lookups.js │ ├── messages.js │ ├── modals.js │ ├── relationships.js │ ├── resources.js │ ├── search.js │ └── templates.js ├── components │ ├── App.jsx │ ├── ClipboardButton.jsx │ ├── Footer.jsx │ ├── Header.jsx │ ├── InputTemplate.jsx │ ├── LongDate.jsx │ ├── ModalWrapper.jsx │ ├── ResourceTemplateChoiceModal.jsx │ ├── RootContainer.jsx │ ├── alerts │ │ ├── Alert.jsx │ │ ├── AlertWrapper.jsx │ │ ├── AlertsContextProvider.jsx │ │ └── ContextAlert.jsx │ ├── buttons │ │ ├── CopyButton.jsx │ │ ├── EditButton.jsx │ │ ├── LoadingButton.jsx │ │ ├── NewButton.jsx │ │ └── ViewButton.jsx │ ├── dashboard │ │ ├── Dashboard.jsx │ │ ├── ResourceList.jsx │ │ ├── SearchList.jsx │ │ └── SearchRow.jsx │ ├── editor │ │ ├── CopyToNewMessage.jsx │ │ ├── Editor.jsx │ │ ├── EditorActions.jsx │ │ ├── ErrorMessages.jsx │ │ ├── ExpiringMessage.jsx │ │ ├── GroupChoiceModal.jsx │ │ ├── ResourceComponent.jsx │ │ ├── ResourceTitle.jsx │ │ ├── ResourceURIMessage.jsx │ │ ├── ResourcesNav.jsx │ │ ├── ResourcesNavTab.jsx │ │ ├── SaveAlert.jsx │ │ ├── ToggleButton.jsx │ │ ├── UnusedRDFDisplay.jsx │ │ ├── actions │ │ │ ├── CloseButton.jsx │ │ │ ├── CloseResourceModal.jsx │ │ │ ├── CopyToNewButton.jsx │ │ │ ├── MarcButton.jsx │ │ │ ├── MarcModal.jsx │ │ │ ├── PermissionsAction.jsx │ │ │ ├── PreviewButton.jsx │ │ │ ├── SaveAndPublishButton.jsx │ │ │ ├── TopButton.jsx │ │ │ ├── TransferButton.jsx │ │ │ └── TransferButtons.jsx │ │ ├── diacritics │ │ │ ├── CharacterButton.jsx │ │ │ ├── DiacriticsSelection.jsx │ │ │ └── VocabChoice.jsx │ │ ├── inputs │ │ │ ├── DiacriticsButton.jsx │ │ │ ├── InputLang.jsx │ │ │ ├── InputListValue.jsx │ │ │ ├── InputLiteralOrURI.jsx │ │ │ ├── InputLiteralValue.jsx │ │ │ ├── InputLookupValue.jsx │ │ │ ├── InputURIValue.jsx │ │ │ ├── LanguageButton.jsx │ │ │ ├── Lookup.jsx │ │ │ ├── LookupTab.jsx │ │ │ ├── LookupTabs.jsx │ │ │ ├── ReadOnlyInputLiteralOrURI.jsx │ │ │ ├── RemoveButton.jsx │ │ │ └── RenderLookupContext.jsx │ │ ├── leftNav │ │ │ ├── ActivePanelPropertyNav.jsx │ │ │ ├── DiffDisplay.jsx │ │ │ ├── DiffModal.jsx │ │ │ ├── LeftNav.jsx │ │ │ ├── PanelResourceNav.jsx │ │ │ ├── PresenceIndicator.jsx │ │ │ ├── PropertySubNav.jsx │ │ │ ├── RelationshipRow.jsx │ │ │ ├── Relationships.jsx │ │ │ ├── RelationshipsDisplay.jsx │ │ │ ├── SubjectSubNav.jsx │ │ │ └── Versions.jsx │ │ ├── preview │ │ │ ├── EditorPreviewModal.jsx │ │ │ ├── PreviewModal.jsx │ │ │ ├── RDFDisplay.jsx │ │ │ ├── ResourceDisplay.jsx │ │ │ ├── ResourcePreviewHeader.jsx │ │ │ └── VersionPreviewModal.jsx │ │ └── property │ │ │ ├── LiteralTypeLabel.jsx │ │ │ ├── NestedProperty.jsx │ │ │ ├── NestedPropertyHeader.jsx │ │ │ ├── NestedResource.jsx │ │ │ ├── NestedResourceActionButtons.jsx │ │ │ ├── PanelProperty.jsx │ │ │ ├── PanelResource.jsx │ │ │ ├── PropertyComponent.jsx │ │ │ ├── PropertyLabel.jsx │ │ │ ├── PropertyLabelInfo.jsx │ │ │ ├── PropertyLabelInfoLink.jsx │ │ │ ├── PropertyLabelInfoTooltip.jsx │ │ │ ├── PropertyPropertyURI.jsx │ │ │ ├── PropertyURI.jsx │ │ │ ├── RequiredSuperscript.jsx │ │ │ ├── ResourceClass.jsx │ │ │ ├── ResourceList.jsx │ │ │ └── ValuePropertyURI.jsx │ ├── exports │ │ └── Exports.jsx │ ├── home │ │ ├── DescPanel.jsx │ │ ├── Header.jsx │ │ ├── HomePage.jsx │ │ ├── LoginPanel.jsx │ │ ├── NewsItem.jsx │ │ ├── NewsPanel.jsx │ │ └── UserNotifications.jsx │ ├── load │ │ ├── LoadByRDFForm.jsx │ │ └── LoadResource.jsx │ ├── menu │ │ └── CanvasMenu.jsx │ ├── metrics │ │ ├── CountCard.jsx │ │ ├── DateRangeFilter.jsx │ │ ├── GroupFilter.jsx │ │ ├── MetricsWrapper.jsx │ │ ├── ResourceCountMetric.jsx │ │ ├── ResourceCreatedCountMetric.jsx │ │ ├── ResourceEditedCountMetric.jsx │ │ ├── ResourceMetrics.jsx │ │ ├── TemplateCountMetric.jsx │ │ ├── TemplateCreatedCountMetric.jsx │ │ ├── TemplateEditedCountMetric.jsx │ │ ├── TemplateFilter.jsx │ │ ├── TemplateMetrics.jsx │ │ ├── TemplateUsageCountMetric.jsx │ │ ├── UserCountMetric.jsx │ │ ├── UserMetrics.jsx │ │ ├── UserResourceCountMetric.jsx │ │ └── UserTemplateCountMetric.jsx │ ├── search │ │ ├── GroupFilter.jsx │ │ ├── HeaderSearch.jsx │ │ ├── QASearchResults.jsx │ │ ├── RelationshipResults.jsx │ │ ├── Search.jsx │ │ ├── SearchFilter.jsx │ │ ├── SearchResultRow.jsx │ │ ├── SearchResultRows.jsx │ │ ├── SearchResultsMessage.jsx │ │ ├── SearchResultsPaging.jsx │ │ ├── SinopiaSearchResults.jsx │ │ ├── SinopiaSort.jsx │ │ ├── TemplateGuessSearchResults.jsx │ │ └── TypeFilter.jsx │ ├── templates │ │ ├── ExpandingResourceTemplates.jsx │ │ ├── NewResourceTemplateButton.jsx │ │ ├── ResourceTemplate.jsx │ │ ├── ResourceTemplateRow.jsx │ │ ├── ResourceTemplateSearchResult.jsx │ │ ├── SinopiaResourceTemplates.jsx │ │ └── TemplateSearch.jsx │ └── vocabulary │ │ └── Vocab.js ├── hooks │ ├── useAlerts.js │ ├── useDiacritics.js │ ├── useEditor.js │ ├── useLeftNav.js │ ├── useMetric.js │ ├── useNavLink.js │ ├── useNavTarget.js │ ├── usePermissions.js │ ├── useRdfResource.js │ ├── useResourcHasChanged.js │ ├── useResource.js │ └── useSearch.js ├── index.js ├── reducers │ ├── authenticate.js │ ├── errors.js │ ├── exports.js │ ├── groups.js │ ├── history.js │ ├── index.js │ ├── languages.js │ ├── lookups.js │ ├── messages.js │ ├── modals.js │ ├── relationships.js │ ├── resourceHelpers.js │ ├── resourceValidationHelpers.js │ ├── resources.js │ ├── search.js │ └── templates.js ├── selectors │ ├── authenticate.js │ ├── errors.js │ ├── exports.js │ ├── groups.js │ ├── history.js │ ├── index.js │ ├── languages.js │ ├── lookups.js │ ├── messages.js │ ├── modals.js │ ├── relationships.js │ ├── resources.js │ ├── search.js │ └── templates.js ├── sinopiaApi.js ├── sinopiaMetrics.js ├── sinopiaSearch.js ├── store.js ├── styles │ ├── bootstrap-override.scss │ ├── bootstrap-variables.scss │ ├── colors.scss │ ├── diff.scss │ ├── editorheaderbg.png │ ├── editorsinopialogo.png │ ├── header.scss │ ├── home-background.png │ ├── language.scss │ ├── lookupContext.scss │ ├── main.scss │ ├── modal.scss │ ├── relationships.scss │ ├── resourceEditor.scss │ ├── resourceNav.scss │ ├── resourceTabs.scss │ └── variables.scss └── utilities │ ├── Bibframe.js │ ├── Language.js │ ├── Lookup.js │ ├── QuestioningAuthority.js │ ├── Search.js │ ├── SinopiaApiHelper.js │ ├── Utilities.js │ ├── authorityConfig.js │ ├── errorKeyFactory.js │ └── valueFactory.js ├── static ├── authorityConfig.json ├── iso639toBCP47.json ├── literalDataType.json ├── literalPropertyAttribute.json ├── propertyAttribute.json ├── propertyType.json ├── resourceAttribute.json ├── searchConfig.json ├── specialcharacters.json ├── templates │ ├── rt_literal_property_attrs_doc.json │ ├── rt_lookup_property_attrs_doc.json │ ├── rt_property_template_doc.json │ ├── rt_resource_property_attrs_doc.json │ ├── rt_resource_template_doc.json │ └── rt_uri_property_attrs_doc.json └── uriAttribute.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | "@babel/plugin-proposal-optional-chaining", 6 | "@babel/plugin-syntax-dynamic-import", 7 | "@babel/plugin-transform-runtime", 8 | [ 9 | "module-resolver", 10 | { 11 | "root": ["./src"] 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | __mocks__ 2 | .circleci 3 | .git 4 | .gitignore 5 | /.eslint*.* 6 | __tests__/__fixtures__/ddc_bad_json.json 7 | build_support 8 | builds 9 | coverage 10 | docs 11 | development.html 12 | jest-puppeteer.config.js 13 | junit.xml 14 | npm-debug.log 15 | setupEnzyme.js 16 | dist 17 | node_modules 18 | .env 19 | cypress.env.json 20 | cypress/screenshots 21 | cypress/videos 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build_support/* 2 | builds/* 3 | coverage/* 4 | docs/* 5 | static/* 6 | dist/* 7 | node_modules/* 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Regular Issue 3 | about: The regular github issue form 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a problem with any part of Sinopia (Editor, Profile Editor) 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | What went wrong? What did you expect to happen? 12 | 13 | - What is the impact on your work? (can't work / work stoppage; workaround exists; minor issue) 14 | - What environment are you in? (stage or production) 15 | - What OS and browser are you using? (Mac/Windows; Safari, Firefox, etc.) 16 | - If you're cataloging, what Resource Template were you using (name or ID) 17 | 18 | **Please include a screenshot** 19 | (Mac: shift-command-4; Windows: ; then drag the image file into the issue) 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or enhancement for any part of Sinopia (Editor, Profile 4 | Editor) 5 | title: '' 6 | labels: enhancement 7 | assignees: '' 8 | 9 | --- 10 | 11 | **Describe the feature you'd like** 12 | 13 | **Give an example** 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/sinopia-alma-integration-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sinopia Alma Integration Request 3 | about: For requesting Sinopia integrations with the Alma LSP 4 | title: "[Sinopia Alma Integration Request]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Institution name:** 11 | 12 | **Contact name and email address for requesting institution:** 13 | 14 | **Name of Sinopia institution group:** 15 | Example: "stanford" 16 | 17 | **Relevant Sinopia environments:** 18 | - [ ] Institution has a Sinopia user group available in development, stage, and production 19 | - [ ] At least one user is registered in the group in each environment 20 | 21 | If your institution user group does not exist, or does not exist in all environments, please specify where your group needs to be added: 22 | - [ ] Stage 23 | - [ ] Production 24 | - [ ] Development 25 | - [ ] Exists in all 3 environments, no additions needed 26 | 27 | **Alma credentials** 28 | Confirm that credentials for the Sinopia / Alma integration can be provided upon request, and provide contact information if different than above. 29 | 30 | **If the above perquisites have been met, please complete the following:** 31 | - [ ] Create a Sinopia Editor PR to add group to src/Config.json [here](https://github.com/LD4P/sinopia_editor/blob/c06d7b49ab4475eafe63874daa8e6c94e5a05402/src/Config.js#L141-L151) 32 | - [ ] Create an ILS-Middleware PR to add group to the alma DAG [here](https://github.com/LD4P/ils-middleware/blob/a2f5206245163209355ddf74ea226a9a369dced1/ils_middleware/dags/alma.py#L48-L60) 33 | - [ ] Add group's credentials to ILS-Middleware 34 | - [ ] Add group's region variables to ILS-Middleware 35 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Why was this change made? 2 | 3 | 4 | 5 | ## How was this change tested? 6 | 7 | 8 | 9 | ## Which documentation and/or configurations were updated? 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | builds 3 | coverage 4 | examples 5 | junit.xml 6 | node_modules 7 | static/profiles/bibframe/* 8 | tmp 9 | dist 10 | # Contains secrets! 11 | .env 12 | cypress.env.json 13 | 14 | test.html 15 | cypress/videos/ 16 | cypress/screenshots/ 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __mocks__ 2 | __tests__ 3 | cypress 4 | *.env* 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .circleci/config.yml 4 | package-lock.json 5 | package.json 6 | docker-compose.yml 7 | babel.config.json 8 | .eslintrc.js 9 | .github 10 | .prettierrc.json 11 | *.md 12 | __tests__/__action_fixtures__/*.json 13 | __tests__/__resource_fixtures__/*.json 14 | __tests__/__template_fixtures__/*.json 15 | cypress/fixtures/*.txt 16 | dist 17 | static/*.json 18 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cimg/node:16.8 2 | 3 | # Allow build-time arguments (for, environment variables that need to be encoded into the webpack distribution) 4 | ARG USE_FIXTURES 5 | ARG SINOPIA_API_BASE_URL=http://localhost:3000 6 | ARG SINOPIA_URI 7 | ARG SINOPIA_ENV 8 | ARG AWS_COGNITO_DOMAIN 9 | ARG COGNITO_CLIENT_ID 10 | ARG COGNITO_USER_POOL_ID 11 | ARG INDEX_URL 12 | ARG EXPORT_BUCKET_URL 13 | ARG HONEYBADGER_API_KEY 14 | ARG HONEYBADGER_REVISION 15 | ENV HONEYBADGER_API_KEY=$HONEYBADGER_API_KEY 16 | 17 | # Set environment variables from the build args 18 | ENV INDEX_URL ${INDEX_URL} 19 | 20 | COPY --chown=circleci:circleci package.json . 21 | COPY --chown=circleci:circleci package-lock.json . 22 | 23 | # Install dependencies 24 | RUN npm install --no-optional 25 | 26 | # Everything that isn't in .dockerignore ships 27 | COPY --chown=circleci:circleci . . 28 | 29 | # Build the app *within* the container because environment variables are fixed at build-time 30 | RUN npm run build 31 | 32 | # Send source map to HB 33 | RUN if [ -n "$HONEYBADGER_API_KEY" ]; then curl https://api.honeybadger.io/v1/source_maps \ 34 | -F api_key=$HONEYBADGER_API_KEY \ 35 | -F revision=$HONEYBADGER_REVISION \ 36 | -F minified_url=$SINOPIA_URI/dist/bundle.js \ 37 | -F source_map=@dist/bundle.js.map \ 38 | -F minified_file=@dist/bundle.js ; fi 39 | 40 | # docker daemon maps app's port 41 | EXPOSE 8000 42 | 43 | CMD ["npm", "start"] 44 | -------------------------------------------------------------------------------- /Dockerfile.cypress: -------------------------------------------------------------------------------- 1 | FROM cypress/included:9.0.0 2 | 3 | COPY cypress /cypress 4 | COPY cypress.json cypress.json 5 | 6 | ENTRYPOINT ["cypress", "run", "--headless", "-b", "chrome"] 7 | -------------------------------------------------------------------------------- /Dockerfile.proxy: -------------------------------------------------------------------------------- 1 | FROM httpd:2.4-alpine 2 | 3 | COPY ./docker/proxy-httpd.conf /usr/local/apache2/conf/httpd.conf 4 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Stanford University see LICENSE for license 2 | module.exports = "test-file-stub" 3 | -------------------------------------------------------------------------------- /__mocks__/mockAWSData.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Stanford University see LICENSE for license 2 | 3 | const mockAWSHeader = { 4 | kid: "x24+y25+z26", 5 | alg: "RS256", 6 | } 7 | const mockAWSReponse = { 8 | sub: "a1b2c3", 9 | token_use: "access", 10 | scope: "openid email", 11 | auth_time: 1234567890, 12 | iss: "https://cognito-idp.fake-region.amazonaws.com/fake-region_1a2b", 13 | exp: 1234567890, 14 | iat: 1234567890, 15 | version: 2, 16 | jti: "1a-2b-3c", 17 | client_id: "1a2b3c", 18 | username: "fake", 19 | } 20 | const mockAWSsigniture = "fake-signature" 21 | const mockHeader = Buffer.from(JSON.stringify(mockAWSHeader)) 22 | .toString("base64") 23 | .slice(0, -2) 24 | const mockToken = Buffer.from(JSON.stringify(mockAWSReponse)) 25 | .toString("base64") 26 | .slice(0, -2) 27 | const mockSignature = Buffer.from(JSON.stringify(mockAWSsigniture)) 28 | .toString("base64") 29 | .slice(0, -2) 30 | const mockJWTString = `${mockHeader}.${mockToken}.${mockSignature}` 31 | 32 | module.exports = mockJWTString 33 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Stanford University see LICENSE for license 2 | module.exports = {} 3 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/addSiblingValueSubject-ADD_VALUE.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | 3 | const build = new ResourceBuilder() 4 | 5 | const expectedAction = { 6 | type: "ADD_VALUE", 7 | payload: { 8 | value: build.subjectValue({ 9 | // "key": "abc2", 10 | propertyKey: "v1o90QO1Qx", 11 | propertyUri: 12 | "http://id.loc.gov/ontologies/bibframe/uber/template1/property1", 13 | valueSubject: build.subject({ 14 | subjectTemplate: build.subjectTemplate({ 15 | id: "resourceTemplate:testing:uber2", 16 | clazz: "http://id.loc.gov/ontologies/bibframe/Uber2", 17 | label: "Uber template2", 18 | remark: 19 | "Template for testing purposes with single repeatable literal with a link to Stanford at https://www.stanford.edu", 20 | propertyTemplates: [ 21 | build.propertyTemplate({ 22 | key: "resourceTemplate:testing:uber2 > http://id.loc.gov/ontologies/bibframe/uber/template2/property1", 23 | subjectTemplateKey: "resourceTemplate:testing:uber2", 24 | label: "Uber template2, property1", 25 | uris: { 26 | "http://id.loc.gov/ontologies/bibframe/uber/template2/property1": 27 | "Property1", 28 | }, 29 | repeatable: true, 30 | remark: "A repeatable literal", 31 | type: "literal", 32 | component: "InputLiteral", 33 | }), 34 | ], 35 | }), 36 | properties: [build.property()], 37 | }), 38 | }), 39 | siblingValueKey: "VDOeQCnFA8", 40 | }, 41 | } 42 | 43 | export default expectedAction 44 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/expandProperty-ADD_PROPERTY.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | 3 | const build = new ResourceBuilder() 4 | 5 | const expectedAction = { 6 | type: "ADD_PROPERTY", 7 | payload: build.property({ 8 | key: "JQEtq-vmq8", 9 | subjectKey: "t9zVwg2zO", 10 | propertyTemplateKey: 11 | "ld4p:RT:bf2:Title:AbbrTitle > http://id.loc.gov/ontologies/bibframe/mainTitle", 12 | show: true, 13 | values: [], 14 | }), 15 | } 16 | 17 | export default expectedAction 18 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/expandProperty-ADD_VALUE.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | 3 | const build = new ResourceBuilder() 4 | 5 | const expectedAction = { 6 | type: "ADD_VALUE", 7 | payload: { 8 | value: build.subjectValue({ 9 | propertyKey: "v1o90QO1Qx", 10 | propertyUri: 11 | "http://id.loc.gov/ontologies/bibframe/uber/template1/property1", 12 | valueSubject: build.subject({ 13 | subjectTemplate: build.subjectTemplate({ 14 | uri: "http://localhost:3000/resource/resourceTemplate:testing:uber2", 15 | id: "resourceTemplate:testing:uber2", 16 | clazz: "http://id.loc.gov/ontologies/bibframe/Uber2", 17 | label: "Uber template2", 18 | remark: 19 | "Template for testing purposes with single repeatable literal.", 20 | propertyTemplates: [ 21 | build.propertyTemplate({ 22 | subjectTemplateKey: "resourceTemplate:testing:uber2", 23 | label: "Uber template2, property1", 24 | uris: { 25 | "http://id.loc.gov/ontologies/bibframe/uber/template2/property1": 26 | "http://id.loc.gov/ontologies/bibframe/uber/template2/property1", 27 | }, 28 | repeatable: true, 29 | remark: "A repeatable literal", 30 | type: "literal", 31 | component: "InputLiteral", 32 | }), 33 | ], 34 | }), 35 | properties: [build.property()], 36 | }), 37 | }), 38 | }, 39 | } 40 | 41 | export default expectedAction 42 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/newResource-ADD_SUBJECT.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | import subjectTemplate from "./subjectTemplate-inputs" 3 | import literalSubjectTemplate from "./subjectTemplate-literal" 4 | 5 | const build = new ResourceBuilder({ injectPropertyKeyIntoValue: true }) 6 | 7 | const expectedAction = { 8 | type: "ADD_SUBJECT", 9 | payload: build.subject({ 10 | subjectTemplate, 11 | properties: [ 12 | build.property({ 13 | values: [], 14 | show: true, 15 | }), 16 | build.property({ 17 | values: [], 18 | show: true, 19 | }), 20 | build.property({ 21 | values: [], 22 | show: true, 23 | }), 24 | build.property({ 25 | values: [], 26 | show: true, 27 | }), 28 | build.property({ 29 | show: true, 30 | values: [ 31 | build.value({ 32 | propertyUri: "http://sinopia.io/testing/Inputs/property5", 33 | valueSubject: build.subject({ 34 | subjectTemplate: literalSubjectTemplate, 35 | properties: [ 36 | build.property({ 37 | values: null, 38 | }), 39 | ], 40 | }), 41 | }), 42 | ], 43 | }), 44 | ], 45 | }), 46 | } 47 | 48 | export default expectedAction 49 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/newResourceCopy-ADD_SUBJECT.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | 3 | const build = new ResourceBuilder({ 4 | injectPropertyKeyIntoValue: true, 5 | injectClassesIntoSubject: true, 6 | }) 7 | 8 | const expectedAction = { 9 | type: "ADD_SUBJECT", 10 | payload: build.subject({ 11 | subjectTemplate: build.subjectTemplate({ 12 | id: "ld4p:RT:bf2:Title:AbbrTitle", 13 | clazz: "http://id.loc.gov/ontologies/bibframe/AbbreviatedTitle", 14 | label: "Abbreviated Title", 15 | author: "LD4P", 16 | date: "2019-08-19", 17 | propertyTemplateKeys: [ 18 | "ld4p:RT:bf2:Title:AbbrTitle > http://id.loc.gov/ontologies/bibframe/mainTitle", 19 | ], 20 | }), 21 | properties: [ 22 | build.property({ 23 | propertyTemplate: build.propertyTemplate({ 24 | subjectTemplateKey: "ld4p:RT:bf2:Title:AbbrTitle", 25 | label: "Abbreviated Title", 26 | uris: { 27 | "http://id.loc.gov/ontologies/bibframe/mainTitle": "Main title", 28 | }, 29 | type: "literal", 30 | component: "InputLiteral", 31 | }), 32 | show: true, 33 | values: [ 34 | build.value({ 35 | literal: "foo", 36 | lang: "en", 37 | component: "InputLiteralValue", 38 | propertyUri: "http://id.loc.gov/ontologies/bibframe/mainTitle", 39 | }), 40 | ], 41 | }), 42 | ], 43 | }), 44 | } 45 | 46 | export default expectedAction 47 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/newResourceFromDataset-ADD_SUBJECT-bad-ordered.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | import orderedSubjectTemplate from "./subjectTemplate-ordered" 3 | 4 | const build = new ResourceBuilder({ injectPropertyKeyIntoValue: true }) 5 | 6 | const expectedAction = { 7 | type: "ADD_SUBJECT", 8 | payload: build.subject({ 9 | uri: "http://localhost:3000/resource/b6c5f4c0-e7cd-4ca5-a20f-2a37fe1080d6", 10 | subjectTemplate: orderedSubjectTemplate, 11 | classes: ["http://sinopia.io/testing/Ordered"], 12 | properties: [ 13 | build.property({ 14 | propertyUri: "http://sinopia.io/testing/Ordered/property1", 15 | }), 16 | ], 17 | }), 18 | } 19 | 20 | export default expectedAction 21 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/newResourceFromDataset-ADD_SUBJECT-nested.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | import suppressibleSubjectTemplate from "./subjectTemplate-suppressible" 3 | import uriSubjectTemplate from "./subjectTemplate-uri" 4 | 5 | const build = new ResourceBuilder({ injectPropertyKeyIntoValue: true }) 6 | 7 | const expectedAction = { 8 | type: "ADD_SUBJECT", 9 | payload: build.subject({ 10 | uri: "http://localhost:3000/resource/b6c5f4c0-e7cd-4ca5-a20f-2a37fe1080d6", 11 | classes: ["http://sinopia.io/testing/Suppressible"], 12 | subjectTemplate: suppressibleSubjectTemplate, 13 | properties: [ 14 | build.property({ 15 | values: [ 16 | build.value({ 17 | propertyUri: "http://sinopia.io/testing/Suppressible/property1", 18 | valueSubject: build.subject({ 19 | subjectTemplate: uriSubjectTemplate, 20 | classes: ["http://sinopia.io/testing/Uri"], 21 | properties: [ 22 | build.property({ 23 | values: [ 24 | build.uriValue({ 25 | propertyUri: "http://sinopia.io/testing/Uri/property1", 26 | uri: "http://foo/bar", 27 | label: "Foo Bar", 28 | }), 29 | ], 30 | }), 31 | ], 32 | }), 33 | }), 34 | ], 35 | }), 36 | ], 37 | }), 38 | } 39 | 40 | export default expectedAction 41 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/subjectTemplate-literal.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | 3 | const build = new ResourceBuilder() 4 | 5 | const subjectTemplate = build.subjectTemplate({ 6 | uri: "http://localhost:3000/resource/resourceTemplate:testing:literal", 7 | id: "resourceTemplate:testing:literal", 8 | clazz: "http://sinopia.io/testing/Literal", 9 | classes: { 10 | "http://sinopia.io/testing/Literal": "Literal", 11 | }, 12 | label: "Literal", 13 | remark: "A template that contains a single literal input.", 14 | propertyTemplates: [ 15 | build.propertyTemplate({ 16 | subjectTemplateKey: "resourceTemplate:testing:literal", 17 | label: "Literal input", 18 | uris: { 19 | "http://sinopia.io/testing/Literal/property1": "Property1", 20 | }, 21 | type: "literal", 22 | component: "InputLiteral", 23 | }), 24 | ], 25 | }) 26 | 27 | export default subjectTemplate 28 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/subjectTemplate-ordered.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | 3 | const build = new ResourceBuilder() 4 | 5 | const subjectTemplate = build.subjectTemplate({ 6 | uri: "http://localhost:3000/resource/resourceTemplate:testing:ordered", 7 | id: "resourceTemplate:testing:ordered", 8 | clazz: "http://sinopia.io/testing/Ordered", 9 | classes: { 10 | "http://sinopia.io/testing/Ordered": "Ordered", 11 | }, 12 | label: "Ordered", 13 | remark: "A template that contains an ordered nested resource.", 14 | propertyTemplates: [ 15 | build.propertyTemplate({ 16 | subjectTemplateKey: "resourceTemplate:testing:ordered", 17 | label: "Ordered nested resource", 18 | uris: { 19 | "http://sinopia.io/testing/Ordered/property1": "Property1", 20 | }, 21 | valueSubjectTemplateKeys: ["resourceTemplate:testing:literal"], 22 | type: "resource", 23 | component: "NestedResource", 24 | ordered: true, 25 | repeatable: true, 26 | }), 27 | ], 28 | }) 29 | 30 | export default subjectTemplate 31 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/subjectTemplate-suppressible.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | 3 | const build = new ResourceBuilder() 4 | 5 | const subjectTemplate = build.subjectTemplate({ 6 | uri: "http://localhost:3000/resource/resourceTemplate:testing:suppressible", 7 | id: "resourceTemplate:testing:suppressible", 8 | clazz: "http://sinopia.io/testing/Suppressible", 9 | classes: { 10 | "http://sinopia.io/testing/Suppressible": "Suppressible", 11 | }, 12 | label: "Suppressible nested resource", 13 | remark: "A template that contains a suppressible nested resource.", 14 | propertyTemplates: [ 15 | build.propertyTemplate({ 16 | subjectTemplateKey: "resourceTemplate:testing:suppressible", 17 | label: "Suppressible nested resource", 18 | uris: { 19 | "http://sinopia.io/testing/Suppressible/property1": "Property1", 20 | }, 21 | valueSubjectTemplateKeys: ["resourceTemplate:testing:suppressedUri"], 22 | type: "resource", 23 | component: "NestedResource", 24 | }), 25 | ], 26 | }) 27 | 28 | export default subjectTemplate 29 | -------------------------------------------------------------------------------- /__tests__/__action_fixtures__/subjectTemplate-uri.js: -------------------------------------------------------------------------------- 1 | import ResourceBuilder from "resourceBuilderUtils" 2 | 3 | const build = new ResourceBuilder() 4 | 5 | const subjectTemplate = build.subjectTemplate({ 6 | uri: "http://localhost:3000/resource/resourceTemplate:testing:suppressedUri", 7 | id: "resourceTemplate:testing:suppressedUri", 8 | clazz: "http://sinopia.io/testing/Uri", 9 | classes: { 10 | "http://sinopia.io/testing/Uri": "URI", 11 | }, 12 | label: "URI", 13 | remark: "A template that contains a single URI input.", 14 | suppressible: true, 15 | propertyTemplates: [ 16 | build.propertyTemplate({ 17 | subjectTemplateKey: "resourceTemplate:testing:suppressedUri", 18 | label: "URI input", 19 | uris: { 20 | "http://sinopia.io/testing/Uri/property1": "Property1", 21 | }, 22 | type: "uri", 23 | component: "InputURI", 24 | }), 25 | ], 26 | }) 27 | 28 | export default subjectTemplate 29 | -------------------------------------------------------------------------------- /__tests__/__resource_fixtures__/instance.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "@id": "http://localhost:3000/resource/9c5bd9f5-1804-45bd-99ed-b6e3774c896e", 4 | "http://sinopia.io/vocabulary/hasResourceTemplate": [ 5 | { 6 | "@value": "resourceTemplate:bf2:Instance" 7 | } 8 | ], 9 | "@type": [ 10 | "http://id.loc.gov/ontologies/bibframe/Instance" 11 | ], 12 | "http://www.w3.org/2000/01/rdf-schema#label": [ 13 | { 14 | "@value": "The Practitioner's Guide to Graph Data", 15 | "@language": "en" 16 | } 17 | ] 18 | } 19 | ] -------------------------------------------------------------------------------- /__tests__/__resource_fixtures__/instance_with_refs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "@id": "http://localhost:3000/resource/a5c5f4c0-e7cd-4ca5-a20f-2a37fe1080d5", 4 | "http://sinopia.io/vocabulary/hasResourceTemplate": [ 5 | { 6 | "@value": "resourceTemplate:bf2:Instance" 7 | } 8 | ], 9 | "@type": [ 10 | "http://id.loc.gov/ontologies/bibframe/Instance" 11 | ], 12 | "http://www.w3.org/2000/01/rdf-schema#label": [ 13 | { 14 | "@value": "Instance1", 15 | "@language": "en" 16 | } 17 | ], 18 | "http://id.loc.gov/ontologies/bibframe/instanceOf": [ 19 | { 20 | "@id": "http://localhost:3000/resource/f6ee6410-5206-492b-8e48-3b6333010c33" 21 | } 22 | ] 23 | }, 24 | { 25 | "@id": "http://localhost:3000/resource/f6ee6410-5206-492b-8e48-3b6333010c33", 26 | "http://www.w3.org/2000/01/rdf-schema#label": [ 27 | { 28 | "@value": "Work1", 29 | "@language": "en" 30 | } 31 | ] 32 | } 33 | ] -------------------------------------------------------------------------------- /__tests__/__resource_fixtures__/invalid_instance.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "@id": "http://localhost:3000/resource/9c5bd9f5-1804-45bd-99ed-b6e3774c896e", 4 | "@type": [ 5 | "http://id.loc.gov/ontologies/bibframe/Instance" 6 | ], 7 | "http://www.w3.org/2000/01/rdf-schema#label": [ 8 | { 9 | "@value": "The Practitioner's Guide to Graph Data", 10 | "@language": "en" 11 | } 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /__tests__/__resource_fixtures__/test2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "@id": "_:b1", 4 | "@type": [ 5 | "http://id.loc.gov/ontologies/bibframe/Uber3" 6 | ], 7 | "http://id.loc.gov/ontologies/bibframe/uber/template3/property1": [ 8 | { 9 | "@value": "Uber template3, property1", 10 | "@language": "en" 11 | } 12 | ], 13 | "http://id.loc.gov/ontologies/bibframe/uber/template3/property2": [ 14 | { 15 | "@value": "Uber template3, property2", 16 | "@language": "en" 17 | } 18 | ] 19 | }, 20 | { 21 | "@id": "http://localhost:3000/resource/a4181509-8046-47c8-9327-6e576c517d70", 22 | "http://sinopia.io/vocabulary/hasResourceTemplate": [ 23 | { 24 | "@value": "resourceTemplate:testing:uber1" 25 | } 26 | ], 27 | "@type": [ 28 | "http://id.loc.gov/ontologies/bibframe/Uber1" 29 | ], 30 | "http://id.loc.gov/ontologies/bibframe/uber/template1/property1": [ 31 | { 32 | "@id": "_:b1" 33 | } 34 | ] 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /__tests__/__template_fixtures__/Note.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "@id": "_:b2", 4 | "@type": [ 5 | "http://sinopia.io/vocabulary/PropertyTemplate" 6 | ], 7 | "http://www.w3.org/2000/01/rdf-schema#label": [ 8 | { 9 | "@value": "Note" 10 | } 11 | ], 12 | "http://sinopia.io/vocabulary/hasPropertyUri": [ 13 | { 14 | "@id": "http://www.w3.org/2000/01/rdf-schema#label" 15 | } 16 | ], 17 | "http://sinopia.io/vocabulary/hasPropertyType": [ 18 | { 19 | "@id": "http://sinopia.io/vocabulary/propertyType/literal" 20 | } 21 | ] 22 | }, 23 | { 24 | "@id": "http://localhost:3000/resource/resourceTemplate:bf2:Note", 25 | "http://sinopia.io/vocabulary/hasResourceTemplate": [ 26 | { 27 | "@value": "sinopia:template:resource" 28 | } 29 | ], 30 | "@type": [ 31 | "http://sinopia.io/vocabulary/ResourceTemplate" 32 | ], 33 | "http://sinopia.io/vocabulary/hasResourceId": [ 34 | { 35 | "@value": "resourceTemplate:bf2:Note" 36 | } 37 | ], 38 | "http://sinopia.io/vocabulary/hasClass": [ 39 | { 40 | "@id": "http://id.loc.gov/ontologies/bibframe/Note" 41 | } 42 | ], 43 | "http://www.w3.org/2000/01/rdf-schema#label": [ 44 | { 45 | "@value": "Note" 46 | } 47 | ], 48 | "http://sinopia.io/vocabulary/hasPropertyTemplate": [ 49 | { 50 | "@list": [ 51 | { 52 | "@id": "_:b2" 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /__tests__/__template_fixtures__/TitleNote.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "@id": "_:b2", 4 | "@type": [ 5 | "http://sinopia.io/vocabulary/PropertyTemplate" 6 | ], 7 | "http://www.w3.org/2000/01/rdf-schema#label": [ 8 | { 9 | "@value": "Note Text" 10 | } 11 | ], 12 | "http://sinopia.io/vocabulary/hasPropertyUri": [ 13 | { 14 | "@id": "http://www.w3.org/2000/01/rdf-schema#label" 15 | } 16 | ], 17 | "http://sinopia.io/vocabulary/hasPropertyAttribute": [ 18 | { 19 | "@id": "http://sinopia.io/vocabulary/propertyAttribute/repeatable" 20 | } 21 | ], 22 | "http://sinopia.io/vocabulary/hasPropertyType": [ 23 | { 24 | "@id": "http://sinopia.io/vocabulary/propertyType/literal" 25 | } 26 | ] 27 | }, 28 | { 29 | "@id": "http://localhost:3000/resource/resourceTemplate:bf2:Title:Note", 30 | "http://sinopia.io/vocabulary/hasResourceTemplate": [ 31 | { 32 | "@value": "sinopia:template:resource" 33 | } 34 | ], 35 | "@type": [ 36 | "http://sinopia.io/vocabulary/ResourceTemplate" 37 | ], 38 | "http://sinopia.io/vocabulary/hasResourceId": [ 39 | { 40 | "@value": "resourceTemplate:bf2:Title:Note" 41 | } 42 | ], 43 | "http://sinopia.io/vocabulary/hasClass": [ 44 | { 45 | "@id": "http://id.loc.gov/ontologies/bibframe/TitleNote" 46 | } 47 | ], 48 | "http://www.w3.org/2000/01/rdf-schema#label": [ 49 | { 50 | "@value": "Title note" 51 | } 52 | ], 53 | "http://sinopia.io/vocabulary/hasPropertyTemplate": [ 54 | { 55 | "@list": [ 56 | { 57 | "@id": "_:b2" 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /__tests__/actionCreators/transfer.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | import configureMockStore from "redux-mock-store" 3 | import thunk from "redux-thunk" 4 | import * as sinopiaApi from "sinopiaApi" 5 | import { createState } from "stateUtils" 6 | import { transfer } from "actionCreators/transfer" 7 | 8 | const mockStore = configureMockStore([thunk]) 9 | 10 | const resourceUri = 11 | "https://api.development.sinopia.io/resource/7b4c275d-b0c7-40a4-80b3-e95a0d9d987c" 12 | 13 | describe("transfer", () => { 14 | describe("successful", () => { 15 | it("dispatches actions to add user", async () => { 16 | sinopiaApi.postTransfer = jest.fn().mockResolvedValue() 17 | const store = mockStore(createState()) 18 | await store.dispatch( 19 | transfer(resourceUri, "stanford", "ils", "testerrorkey") 20 | ) 21 | 22 | expect(store.getActions()).toHaveLength(0) 23 | expect(sinopiaApi.postTransfer).toHaveBeenCalledWith( 24 | resourceUri, 25 | "stanford", 26 | "ils" 27 | ) 28 | }) 29 | }) 30 | describe("failure", () => { 31 | it("dispatches actions to remove user", async () => { 32 | sinopiaApi.postTransfer = jest.fn().mockRejectedValue("Ooops!") 33 | const store = mockStore(createState()) 34 | await store.dispatch( 35 | transfer(resourceUri, "stanford", "ils", "testerrorkey") 36 | ) 37 | 38 | expect(store.getActions()).toHaveAction("ADD_ERROR", { 39 | errorKey: "testerrorkey", 40 | error: "Error requesting transfer: Ooops!", 41 | }) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /__tests__/components/LongDate.test.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import LongDate from "components/LongDate" 3 | import { render, screen } from "@testing-library/react" 4 | 5 | describe("", () => { 6 | it("uses the provided timeZone", async () => { 7 | render() 8 | screen.getByText("Sep 21, 2012") 9 | }) 10 | 11 | it("handles invalid dates", async () => { 12 | const { container } = render() 13 | expect(container).toBeEmpty() 14 | }) 15 | 16 | it("does not render if null", async () => { 17 | const { container } = render() 18 | expect(container).toBeEmpty() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /__tests__/components/editor/preview/ResourcePreviewHeader.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { createStore, renderComponent } from "testUtils" 5 | import { screen } from "@testing-library/react" 6 | import { createState } from "stateUtils" 7 | import { selectNormSubject } from "selectors/resources" 8 | import ResourcePreviewHeader from "components/editor/preview/ResourcePreviewHeader" 9 | 10 | describe("", () => { 11 | it("displays label, url and edit groups", () => { 12 | const state = createState({ hasTwoLiteralResources: true }) 13 | const store = createStore(state) 14 | const resource = selectNormSubject(state, "t9zVwg2zO") 15 | renderComponent(, store) 16 | expect(screen.getByText("Abbreviated Title")).toBeTruthy // label is shown 17 | expect(screen.getByText("Stanford University")).toBeTruthy // owner is shown with full group name 18 | expect(screen.getByText("Cornell University")).toBeTruthy // editable groups are shown with full group name 19 | expect(screen.queryByTestId("expand-groups-button")).not.toBeInTheDocument // expand button is not shown (since there is only edit group to be shown) 20 | expect(screen.getByText(/(https:\/\/api.sinopia.io\/resource\/0894a8b3)/)) 21 | .toBeTruthy // URI is shown 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /__tests__/components/editor/property/PropertyLabelInfoTooltip.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { createStore, renderComponent } from "testUtils" 5 | import { screen } from "@testing-library/react" 6 | import { createState } from "stateUtils" 7 | import { selectSubjectAndPropertyTemplates } from "selectors/templates" 8 | import PropertyLabelInfoTooltip from "components/editor/property/PropertyLabelInfoTooltip" 9 | 10 | describe("", () => { 11 | it("displays remark without link", () => { 12 | const state = createState({ hasResourceWithNestedResource: true }) 13 | const store = createStore(state) 14 | const propertyTemplate = selectSubjectAndPropertyTemplates( 15 | state, 16 | "resourceTemplate:testing:uber1" 17 | ) 18 | renderComponent( 19 | , 20 | store 21 | ) 22 | expect(screen.getByRole("link").getAttribute("data-bs-content")).toBe( 23 | "Template for testing purposes." 24 | ) 25 | }) 26 | 27 | it("displays remark with a URL that is auto linked", () => { 28 | const state = createState({ hasResourceWithNestedResource: true }) 29 | const store = createStore(state) 30 | const propertyTemplate = selectSubjectAndPropertyTemplates( 31 | state, 32 | "resourceTemplate:testing:uber2" 33 | ) 34 | renderComponent( 35 | , 36 | store 37 | ) 38 | expect(screen.getByRole("link").getAttribute("data-bs-content")).toBe( 39 | 'Template for testing purposes with single repeatable literal with a link to Stanford at https://www.stanford.edu' 40 | ) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /__tests__/components/home/UserNotifications.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { screen } from "@testing-library/react" 5 | import UserNotifications from "components/home/UserNotifications" 6 | import { createStore, renderComponent } from "testUtils" 7 | import { createState } from "stateUtils" 8 | 9 | describe("", () => { 10 | it("shows a warning if a logged in user is not in any groups", async () => { 11 | const state = createState({ noGroups: true }) 12 | const store = createStore(state) 13 | const { container } = renderComponent(, store) 14 | await screen.findByText(/Before you can create/) 15 | expect(container).not.toBeNull 16 | }) 17 | 18 | it("does not show a warning if a logged in user is in at least one group", async () => { 19 | const state = createState() 20 | const store = createStore(state) 21 | const { container } = renderComponent(, store) 22 | expect(container).toBeNull 23 | }) 24 | 25 | it("does not show a warning if there is no logged in user", async () => { 26 | const state = createState({ notAuthenticated: true }) 27 | const store = createStore(state) 28 | const { container } = renderComponent(, store) 29 | expect(container).toBeNull 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /__tests__/feature/editing/changeGroups.test.js: -------------------------------------------------------------------------------- 1 | import { renderApp, createStore, createHistory } from "testUtils" 2 | import { fireEvent, screen } from "@testing-library/react" 3 | import { createState } from "stateUtils" 4 | import { featureSetup, resourceHeaderSelector } from "featureUtils" 5 | import * as sinopiaApi from "sinopiaApi" 6 | 7 | featureSetup() 8 | 9 | const history = createHistory([ 10 | "/editor/resource/b6c5f4c0-e7cd-4ca5-a20f-2a37fe1080d6", 11 | ]) 12 | 13 | jest.spyOn(sinopiaApi, "putResource").mockResolvedValue(true) 14 | 15 | describe("user that can edit, but not an owner, can view groups", () => { 16 | it("user changes groups", async () => { 17 | const state = createState() 18 | const store = createStore(state) 19 | renderApp(store, history) 20 | 21 | await screen.findByText("Inputs", { selector: resourceHeaderSelector }) 22 | fireEvent.click(screen.getByText("Permissions")) 23 | 24 | // Change the owner 25 | const ownerSelect = await screen.findByTestId("Who owns this?") 26 | expect(ownerSelect).toHaveValue("stanford") 27 | fireEvent.change(ownerSelect, { target: { value: "pcc" } }) 28 | expect(ownerSelect).toHaveValue("pcc") 29 | 30 | // Select editing groups 31 | screen.getByText("Who else can edit?") 32 | fireEvent.click(screen.getByText("Cornell University")) 33 | fireEvent.click(screen.getByText("Duke University")) 34 | screen.getByText("Cornell University, Duke University") 35 | 36 | // A modal for group choice and save appears 37 | const modalSave = screen.getByTestId("Save Group") 38 | fireEvent.click(modalSave) 39 | await screen.findByText("Saved") 40 | }, 25000) 41 | }) 42 | -------------------------------------------------------------------------------- /__tests__/feature/editing/editingImmutable.test.js: -------------------------------------------------------------------------------- 1 | import { renderApp, createHistory } from "testUtils" 2 | import { fireEvent, screen } from "@testing-library/react" 3 | import * as sinopiaApi from "sinopiaApi" 4 | import { featureSetup, resourceHeaderSelector } from "featureUtils" 5 | 6 | featureSetup() 7 | 8 | jest 9 | .spyOn(sinopiaApi, "postResource") 10 | .mockResolvedValue( 11 | "http://localhost:3000/resource/c7db5404-7d7d-40ac-b38e-c821d2c3ae3f" 12 | ) 13 | 14 | describe("editing an immutable property", () => { 15 | const history = createHistory(["/editor/resourceTemplate:bf2:Note:Immutable"]) 16 | renderApp(null, history) 17 | 18 | it("allows editing before save, but not after save", async () => { 19 | await screen.findByText("Immutable note", { 20 | selector: resourceHeaderSelector, 21 | }) 22 | 23 | const saveBtn = screen.getAllByText("Save", { selector: "button" })[0] // there are multiple save buttons, grab the first 24 | 25 | const input = screen.getByPlaceholderText("Note") 26 | fireEvent.change(input, { target: { value: "foo" } }) 27 | fireEvent.keyDown(input, { key: "Enter", code: 13, charCode: 13 }) 28 | 29 | expect(saveBtn).not.toBeDisabled() 30 | fireEvent.click(saveBtn) 31 | 32 | // A modal for group choice and save appears 33 | const modalSave = screen.getByRole("button", { name: "Save Group" }) 34 | fireEvent.click(modalSave) 35 | // The resource is saved and is assigned a URI 36 | await screen.findAllByText(/URI for this resource/) 37 | 38 | // No longer editable 39 | expect(screen.queryByPlaceholderText("Note")).not.toBeInTheDocument() 40 | screen.getByText("foo [en]") 41 | }, 10000) 42 | }) 43 | -------------------------------------------------------------------------------- /__tests__/feature/editing/expandContract.test.js: -------------------------------------------------------------------------------- 1 | import { renderApp, createHistory } from "testUtils" 2 | import { fireEvent, screen } from "@testing-library/react" 3 | import { featureSetup, resourceHeaderSelector } from "featureUtils" 4 | 5 | featureSetup() 6 | 7 | describe("expanding and contracting properties", () => { 8 | it("shows and hides nested properties", async () => { 9 | const history = createHistory(["/editor/resourceTemplate:testing:uber1"]) 10 | renderApp(null, history) 11 | 12 | await screen.findByText("Uber template1", { 13 | selector: resourceHeaderSelector, 14 | }) 15 | 16 | // Get rid of Uber template1, property3 17 | fireEvent.click(screen.getByTestId("Remove Uber template1, property3")) 18 | 19 | await screen.findByText("Uber template2", { selector: "h5" }) 20 | await screen.findByText("Uber template3", { selector: "h5" }) 21 | 22 | // Add a nested property 23 | fireEvent.click(screen.getByTestId("Add Uber template2, property1")) 24 | // Input box displayed 25 | await screen.findByPlaceholderText("Uber template2, property1") 26 | 27 | // Hide 28 | fireEvent.click(screen.getByTestId("Hide Uber template2, property1")) 29 | // Input box not displayed 30 | expect( 31 | screen.queryAllByPlaceholderText("Uber template2, property1") 32 | ).toHaveLength(0) 33 | 34 | // Show 35 | fireEvent.click(screen.getByTestId("Show Uber template2, property1")) 36 | // Input box displayed 37 | await screen.findByPlaceholderText("Uber template2, property1") 38 | }, 30000) 39 | }) 40 | -------------------------------------------------------------------------------- /__tests__/feature/editing/propInfo.test.js: -------------------------------------------------------------------------------- 1 | import { renderApp, createHistory } from "testUtils" 2 | import { screen } from "@testing-library/react" 3 | import { featureSetup } from "featureUtils" 4 | 5 | featureSetup() 6 | 7 | describe("getting property related info from a resource", () => { 8 | it("has tooltip text info or a link based on the content of a top-level property remark", async () => { 9 | const history = createHistory(["/editor/resourceTemplate:testing:uber3"]) 10 | renderApp(null, history) 11 | 12 | // if the tooltip remark is text 13 | const infoIcon3 = await screen.findByRole("link", { 14 | name: "Uber template3, property1", 15 | }) 16 | expect(infoIcon3).toHaveAttribute("data-bs-content", "A literal") 17 | 18 | const infoLink = await screen.findByRole("link", { 19 | name: "http://access.rdatoolkit.org/1.0.html", 20 | }) 21 | 22 | expect(infoLink).toHaveClass("prop-remark") 23 | }) 24 | 25 | it("has tooltip text info based on the content of a nested property remark", async () => { 26 | const history = createHistory(["/editor/resourceTemplate:testing:uber1"]) 27 | renderApp(null, history) 28 | 29 | // Finds the parent property 30 | const infoIcon1 = await screen.findByRole("link", { 31 | name: "Uber template1, property18", 32 | }) 33 | expect(infoIcon1).toHaveAttribute( 34 | "data-bs-content", 35 | "Mandatory nested resource templates." 36 | ) 37 | 38 | // Finds the nested property info (tooltip remark is text) 39 | const nestedInfoIcons = await screen.findAllByRole("link", { 40 | name: "Uber template4, property1", 41 | }) 42 | expect(nestedInfoIcons[0]).toHaveAttribute( 43 | "data-bs-content", 44 | "A repeatable, required literal" 45 | ) 46 | }, 15000) 47 | }) 48 | -------------------------------------------------------------------------------- /__tests__/feature/editing/viewGroups.test.js: -------------------------------------------------------------------------------- 1 | import { renderApp, createStore } from "testUtils" 2 | import { fireEvent, screen } from "@testing-library/react" 3 | import { createState } from "stateUtils" 4 | import { featureSetup, resourceHeaderSelector } from "featureUtils" 5 | 6 | featureSetup() 7 | 8 | describe("user that can edit, but not an owner, can view groups", () => { 9 | const uri = 10 | "http://localhost:3000/resource/c7db5404-7d7d-40ac-b38e-c821d2c3ae3f" 11 | 12 | it("is a read-only view of groups", async () => { 13 | const state = createState({ editGroups: true }) 14 | const store = createStore(state) 15 | renderApp(store) 16 | 17 | fireEvent.click(screen.getByText("Linked Data Editor", { selector: "a" })) 18 | 19 | fireEvent.change(screen.getByLabelText("Search"), { 20 | target: { value: uri }, 21 | }) 22 | fireEvent.click(screen.getByTestId("Submit search")) 23 | 24 | await screen.findByText(uri) 25 | fireEvent.click(screen.getByRole("button", { name: `Edit ${uri}` })) 26 | 27 | await screen.findByText("Example Label", { 28 | selector: resourceHeaderSelector, 29 | }) 30 | 31 | fireEvent.click(screen.getByText("Permissions")) 32 | 33 | await screen.findByText("Who owns this?") 34 | screen.getByText("Stanford University", { selector: "p" }) 35 | screen.getByText("Cornell University", { selector: "p" }) 36 | expect(screen.queryByLabelText("Save Group")).not.toBeInTheDocument() 37 | 38 | // Click cancel 39 | fireEvent.click(screen.getByLabelText("Cancel Save Group")) 40 | }, 20000) 41 | }) 42 | -------------------------------------------------------------------------------- /__tests__/feature/exportFile.test.js: -------------------------------------------------------------------------------- 1 | import { renderApp, createHistory } from "testUtils" 2 | import { screen } from "@testing-library/react" 3 | import Config from "Config" 4 | 5 | describe("downloading a file that was exported from Sinopia AWS", () => { 6 | const history = createHistory(["/exports"]) 7 | 8 | it("has a list of the downloadable zip files that were built", () => { 9 | renderApp(null, history) 10 | 11 | const link1 = screen.getByText( 12 | "sinopia_export_all_2020-01-01T00:00:00.000Z.zip" 13 | ) 14 | const link2 = screen.getByText("stanford_2020-01-01T00:00:00.000Z.zip") 15 | 16 | expect(link1.href).toBe( 17 | `${Config.exportBucketUrl}/sinopia_export_all_2020-01-01T00:00:00.000Z.zip` 18 | ) 19 | 20 | expect(link2.href).toBe( 21 | `${Config.exportBucketUrl}/stanford_2020-01-01T00:00:00.000Z.zip` 22 | ) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /__tests__/feature/newResourceTemplate.test.js: -------------------------------------------------------------------------------- 1 | import { renderApp } from "testUtils" 2 | import { fireEvent, screen } from "@testing-library/react" 3 | import { featureSetup, resourceHeaderSelector } from "featureUtils" 4 | 5 | featureSetup() 6 | 7 | describe("creating new resource template ", () => { 8 | it("opens the resource template for the resource", async () => { 9 | renderApp() 10 | 11 | fireEvent.click(screen.getByText("Linked Data Editor", { selector: "a" })) 12 | fireEvent.click(screen.getByText("Resource Templates", { selector: "a" })) 13 | 14 | // Click the new resource template button 15 | fireEvent.click(screen.getByText("New template")) 16 | await screen.findByText( 17 | "Resource template", 18 | { 19 | selector: resourceHeaderSelector, 20 | }, 21 | { timeout: 15000 } 22 | ) 23 | }, 20000) 24 | }) 25 | -------------------------------------------------------------------------------- /__tests__/feature/readNews.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Stanford University see LICENSE for license 2 | 3 | import { renderApp } from "testUtils" 4 | import { screen } from "@testing-library/react" 5 | 6 | describe("reading the latest Sinopia news items", () => { 7 | it("displays a div containing the latest news, version of Sinopia, link to github site, and a static text list", async () => { 8 | renderApp() 9 | await screen.findByText("Latest news") 10 | await screen.findByText(/Sinopia Version \d+\.\d+\.\d+ highlights/) 11 | screen.getByText("Sinopia help site", { selector: "a" }) 12 | expect(document.querySelectorAll("li").length).toBeGreaterThanOrEqual(1) // eslint-disable-line testing-library/no-node-access 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /__tests__/feature/unusedRDF.test.js: -------------------------------------------------------------------------------- 1 | import { renderApp } from "testUtils" 2 | import { fireEvent, screen } from "@testing-library/react" 3 | import { featureSetup, resourceHeaderSelector } from "featureUtils" 4 | 5 | featureSetup() 6 | 7 | describe("loading RDF with unused triples", () => { 8 | it("opens the resource and displays unused triples", async () => { 9 | renderApp() 10 | 11 | fireEvent.click(screen.getByText("Linked Data Editor", { selector: "a" })) 12 | fireEvent.click(screen.getByText("Load RDF", { selector: "a" })) 13 | 14 | screen.getByText("Load RDF into Editor") 15 | 16 | const rdf = ` 17 | <> "foo"@en . 18 | <> "ld4p:RT:bf2:Title:AbbrTitle" . 19 | <> . 20 | <> "bar"@en . 21 | ` 22 | 23 | fireEvent.change( 24 | screen.getByLabelText( 25 | "RDF (Accepts JSON-LD, Turtle, TriG, N-Triples, N-Quads, and Notation3 (N3))" 26 | ), 27 | { target: { value: rdf } } 28 | ) 29 | fireEvent.click(screen.getByText("Submit")) 30 | 31 | await screen.findByText("Abbreviated Title", { 32 | selector: resourceHeaderSelector, 33 | }) 34 | 35 | screen.getByText(/Unable to load the entire resource/) 36 | 37 | // Switch to turtle 38 | fireEvent.change(screen.getByLabelText(/Format/), { 39 | target: { value: "turtle" }, 40 | }) 41 | 42 | await screen.findByText('<> "bar"@en.') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /__tests__/matchers/actions.js: -------------------------------------------------------------------------------- 1 | import { pretty } from "./matcherUtils" 2 | import _ from "lodash" 3 | 4 | expect.extend({ 5 | toHaveAction(actions, actionType, payload) { 6 | if (typeof actionType !== "string") { 7 | throw new Error("expected actionType to be a string") 8 | } 9 | 10 | if ( 11 | actions.findIndex((action) => { 12 | if (action.type !== actionType) return false 13 | if (!payload) return true 14 | return _.isEqual(payload, action.payload) 15 | }) === -1 16 | ) { 17 | return { 18 | pass: false, 19 | message: () => formatMsg(actions, actionType, payload, false), 20 | } 21 | } 22 | 23 | return { 24 | pass: true, 25 | message: () => formatMsg(actions, actionType, payload, true), 26 | } 27 | }, 28 | }) 29 | 30 | const formatMsg = (actions, actionType, payload, notToHave) => { 31 | let msg = `Expected ${pretty(actions)}` 32 | if (notToHave) msg = `${msg} not` 33 | msg = `${msg} to have ${actionType}` 34 | if (payload) msg = `${msg} with payload ${pretty(payload)}` 35 | return msg 36 | } 37 | -------------------------------------------------------------------------------- /__tests__/matchers/matcherUtils.js: -------------------------------------------------------------------------------- 1 | // From https://stackoverflow.com/questions/11616630/how-can-i-print-a-circular-structure-in-a-json-like-format 2 | JSON.safeStringify = (obj, indent = 2) => { 3 | let cache = [] 4 | const retVal = JSON.stringify( 5 | obj, 6 | (key, value) => 7 | typeof value === "object" && value !== null 8 | ? cache.includes(value) 9 | ? undefined // Duplicate reference found, discard key 10 | : cache.push(value) && value // Store value in our collection 11 | : value, 12 | indent 13 | ) 14 | cache = null 15 | return retVal 16 | } 17 | 18 | export const pretty = (obj) => JSON.safeStringify(obj) 19 | 20 | export const noop = () => {} 21 | -------------------------------------------------------------------------------- /__tests__/reducers/authenticate.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Stanford University see LICENSE for license 2 | import { setUser, removeUser } from "reducers/authenticate" 3 | 4 | describe("setUser()", () => { 5 | it("adds user to state", () => { 6 | const state = { 7 | user: undefined, 8 | } 9 | expect(setUser(state, { payload: { username: "jfoo" } })).toEqual({ 10 | user: { 11 | username: "jfoo", 12 | }, 13 | }) 14 | }) 15 | }) 16 | 17 | describe("removeUser()", () => { 18 | it("removes user from state", () => { 19 | const state = { 20 | user: { 21 | username: "jfoo", 22 | }, 23 | } 24 | expect(removeUser(state)).toEqual({}) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /__tests__/reducers/exports.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Stanford University see LICENSE for license 2 | import { exportsReceived } from "reducers/exports" 3 | import { createReducer } from "reducers/index" 4 | import { createState } from "stateUtils" 5 | 6 | const handlers = { 7 | EXPORTS_RECEIVED: exportsReceived, 8 | } 9 | const reducer = createReducer(handlers) 10 | 11 | describe("exportsReceived", () => { 12 | const exportFilenames = [ 13 | "alberta_2020-08-23T00:01:15.272Z.zip", 14 | "boulder_2020-08-23T00:01:14.781Z.zip", 15 | ] 16 | 17 | it("sets the list of export file names", () => { 18 | const action = { 19 | type: "EXPORTS_RECEIVED", 20 | payload: exportFilenames, 21 | } 22 | 23 | const oldState = createState() 24 | const newState = reducer(oldState.entities, action) 25 | expect(newState).toMatchObject({ 26 | exports: exportFilenames, 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /__tests__/reducers/lookups.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Stanford University see LICENSE for license 2 | import { lookupOptionsRetrieved } from "reducers/lookups" 3 | import { createState } from "stateUtils" 4 | 5 | describe("lookupOptionsRetrieved", () => { 6 | const uri = "https://id.loc.gov/vocabulary/mgroove" 7 | const lookup = [ 8 | { 9 | id: "sFdbC6NLsZ", 10 | label: "Lateral or combined cutting", 11 | uri: "http://id.loc.gov/vocabulary/mgroove/lateral", 12 | }, 13 | { 14 | id: "mDg4LzQtGH", 15 | label: "Coarse groove", 16 | uri: "http://id.loc.gov/vocabulary/mgroove/coarse", 17 | }, 18 | ] 19 | 20 | it("adds a new lookup", () => { 21 | const newState = lookupOptionsRetrieved(createState().entities, { 22 | payload: { uri, lookup }, 23 | }) 24 | expect(newState).toMatchObject({ 25 | lookups: { 26 | [uri]: [ 27 | { 28 | id: "mDg4LzQtGH", 29 | label: "Coarse groove", 30 | uri: "http://id.loc.gov/vocabulary/mgroove/coarse", 31 | }, 32 | { 33 | id: "sFdbC6NLsZ", 34 | label: "Lateral or combined cutting", 35 | uri: "http://id.loc.gov/vocabulary/mgroove/lateral", 36 | }, 37 | ], 38 | }, 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /__tests__/reducers/messages.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Stanford University see LICENSE for license 2 | 3 | import { showCopyNewMessage } from "reducers/messages" 4 | import { createReducer } from "reducers/index" 5 | 6 | describe("showCopyNewMessage()", () => { 7 | const handlers = { SHOW_COPY_NEW_MESSAGE: showCopyNewMessage } 8 | const reducer = createReducer(handlers) 9 | 10 | it("copies new message when payload has an URI", () => { 11 | const oldState = { 12 | copyToNewMessage: {}, 13 | } 14 | const action = { 15 | type: "SHOW_COPY_NEW_MESSAGE", 16 | payload: { 17 | oldUri: "https://sinopia.io/stanford/1234", 18 | timestamp: 1594667068562, 19 | }, 20 | } 21 | 22 | const newState = reducer(oldState, action) 23 | expect(newState).toStrictEqual({ 24 | copyToNewMessage: { 25 | timestamp: 1594667068562, 26 | oldUri: "https://sinopia.io/stanford/1234", 27 | }, 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /__tests__/selectors/authenticate.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import { hasUser, selectUser } from "selectors/authenticate" 4 | 5 | const stateWithUser = { 6 | authenticate: { 7 | user: { 8 | username: "jfoo", 9 | }, 10 | }, 11 | } 12 | 13 | const stateWithoutUser = { 14 | authenticate: {}, 15 | } 16 | 17 | describe("hasUser()", () => { 18 | describe("when there is a user", () => { 19 | it("returns true", () => { 20 | expect(hasUser(stateWithUser)).toBe(true) 21 | }) 22 | }) 23 | describe("when no user", () => { 24 | it("returns false", () => { 25 | expect(hasUser(stateWithoutUser)).toBe(false) 26 | }) 27 | }) 28 | }) 29 | 30 | describe("selectUser()", () => { 31 | it("returns user", () => { 32 | expect(selectUser(stateWithUser)).toEqual({ 33 | username: "jfoo", 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/selectors/templates.test.js: -------------------------------------------------------------------------------- 1 | import { createState } from "stateUtils" 2 | import { selectSubjectAndPropertyTemplates } from "selectors/templates" 3 | 4 | describe("selectSubjectAndPropertyTemplates()", () => { 5 | it("returns null when no subject", () => { 6 | const state = createState() 7 | expect(selectSubjectAndPropertyTemplates(state, "abc123")).toEqual(null) 8 | }) 9 | 10 | it("returns templates", () => { 11 | const state = createState({ hasResourceWithLiteral: true }) 12 | const subjectTemplate = selectSubjectAndPropertyTemplates( 13 | state, 14 | "ld4p:RT:bf2:Title:AbbrTitle" 15 | ) 16 | expect(subjectTemplate).toBeSubjectTemplate("ld4p:RT:bf2:Title:AbbrTitle") 17 | expect(subjectTemplate.propertyTemplates).toBePropertyTemplates([ 18 | "ld4p:RT:bf2:Title:AbbrTitle > http://id.loc.gov/ontologies/bibframe/mainTitle", 19 | ]) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /__tests__/testUtilities/actionUtils.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash" 2 | 3 | // This removes circular references. 4 | export const safeAction = (action) => JSON.parse(JSON.safeStringify(action)) 5 | 6 | export const cloneAddResourceActionAsNewResource = (addResourceAction) => { 7 | const clonedAction = _.cloneDeep(addResourceAction) 8 | 9 | clonedAction.payload.uri = null 10 | clonedAction.payload.group = null 11 | clonedAction.payload.editGroups = [] 12 | 13 | return clonedAction 14 | } 15 | -------------------------------------------------------------------------------- /__tests__/testUtilities/featureUtils.js: -------------------------------------------------------------------------------- 1 | import Config from "Config" 2 | import * as sinopiaApi from "sinopiaApi" 3 | import * as sinopiaSearch from "sinopiaSearch" 4 | 5 | export const featureSetup = (opts = {}) => { 6 | jest.spyOn(Config, "useResourceTemplateFixtures", "get").mockReturnValue(true) 7 | jest.spyOn(Config, "useLanguageFixtures", "get").mockReturnValue(true) 8 | // Mock out document.elementFromPoint used by useNavigableComponent. 9 | global.document.elementFromPoint = jest.fn() 10 | // Mock out scrollIntoView used by useNavigableComponent. See https://github.com/jsdom/jsdom/issues/1695 11 | Element.prototype.scrollIntoView = jest.fn() 12 | window.scrollTo = jest.fn() 13 | // Mock out so does not try to update API. 14 | if (!opts.noMockSinopiaApi) 15 | jest.spyOn(sinopiaApi, "putUserHistory").mockResolvedValue() 16 | if (!opts.noMockSinopiaSearch) 17 | jest 18 | .spyOn(sinopiaSearch, "getSearchResultsByUris") 19 | .mockResolvedValue({ results: [] }) 20 | } 21 | 22 | export const resourceHeaderSelector = "h3#resource-header span" 23 | -------------------------------------------------------------------------------- /__tests__/testUtilities/resolver.js: -------------------------------------------------------------------------------- 1 | module.exports = (path, options) => { 2 | // Call the defaultResolver, so we leverage its cache, error handling, etc. 3 | return options.defaultResolver(path, { 4 | ...options, 5 | // Use packageFilter to process parsed `package.json` before the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb) 6 | packageFilter: (pkg) => { 7 | // This is a workaround for https://github.com/uuidjs/uuid/pull/616 8 | // 9 | // jest-environment-jsdom 28+ tries to use browser exports instead of default exports, 10 | // but uuid and nanoid only offers an ESM browser export and not a CommonJS one. Jest does not yet 11 | // support ESM modules natively, so this causes a Jest error related to trying to parse 12 | // "export" syntax. 13 | // 14 | // This workaround prevents Jest from considering uuid and nanoid's module-based exports at all; 15 | // it falls back to uuid's CommonJS+node "main" property. 16 | // 17 | // This is based on https://github.com/microsoft/accessibility-insights-web/pull/5421/commits/9ad4e618019298d82732d49d00aafb846fb6bac7 18 | // See https://github.com/microsoft/accessibility-insights-web/pull/5421#issuecomment-1109168149 19 | if (pkg.name === "nanoid" || pkg.name === "uuid") { 20 | delete pkg.exports 21 | delete pkg.module 22 | } 23 | return pkg 24 | }, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /__tests__/utilities/Language.test.js: -------------------------------------------------------------------------------- 1 | import { parseLangTag, stringifyLangTag } from "utilities/Language" 2 | 3 | describe("Language", () => { 4 | describe("parseLangTag()", () => { 5 | it("parses lang only", () => { 6 | expect(parseLangTag("ja")).toEqual(["ja", null, null]) 7 | }) 8 | it("parses lang and script", () => { 9 | expect(parseLangTag("ja-Latn")).toEqual(["ja", "Latn", null]) 10 | }) 11 | it("parses lang, script, and transliteration", () => { 12 | expect(parseLangTag("ja-Latn-t-ja-m0-alaloc")).toEqual([ 13 | "ja", 14 | "Latn", 15 | "alaloc", 16 | ]) 17 | }) 18 | it("parses lang and transliteration", () => { 19 | expect(parseLangTag("ja-t-ja-m0-alaloc")).toEqual(["ja", null, "alaloc"]) 20 | }) 21 | }) 22 | 23 | describe("stringifyLangTag()", () => { 24 | it("stringifies lang only", () => { 25 | expect(stringifyLangTag("ja", null, null)).toEqual("ja") 26 | }) 27 | it("stringifies lang and script", () => { 28 | expect(stringifyLangTag("ja", "Latn", null)).toEqual("ja-Latn") 29 | }) 30 | it("stringifies lang, script, and transliteration", () => { 31 | expect(stringifyLangTag("ja", "Latn", "alaloc")).toEqual( 32 | "ja-Latn-t-ja-m0-alaloc" 33 | ) 34 | }) 35 | it("stringifies lang and transliteration", () => { 36 | expect(stringifyLangTag("ja", null, "alaloc")).toEqual( 37 | "ja-t-ja-m0-alaloc" 38 | ) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands" 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sinopia Linked Data Editor 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /react-testing-library.setup.js: -------------------------------------------------------------------------------- 1 | // See https://github.com/kentcdodds/react-testing-library#global-config 2 | // import 'jest-dom/extend-expect'; 3 | import "@testing-library/jest-dom/extend-expect" 4 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Stanford University see LICENSE for license 3 | * 4 | * Minimal BIBFRAME Editor Node.js server. To run from the command-line: 5 | * npm start or node server.js 6 | */ 7 | 8 | import express from "express" 9 | import Config from "./src/Config" 10 | 11 | import cors from "cors" 12 | import proxy from "express-http-proxy" 13 | 14 | const port = 8000 15 | const app = express() 16 | 17 | app.use(express.urlencoded({ extended: true })) // handle URL-encoded data 18 | 19 | app.use(cors()) 20 | app.options("*", cors()) 21 | 22 | app.use( 23 | "/api/search", 24 | proxy(Config.indexUrl, { 25 | parseReqBody: false, 26 | proxyReqOptDecorator(proxyReqOpts) { 27 | delete proxyReqOpts.headers.origin 28 | return proxyReqOpts 29 | }, 30 | filter: (req) => req.method === "POST", 31 | }) 32 | ) 33 | 34 | app.get("/", (req, res) => { 35 | res.sendFile(`${__dirname}/dist/index.html`) 36 | }) 37 | 38 | // Serve static assets to the browser, e.g., from src/styles/ and static/ 39 | app.use(express.static(`${__dirname}/`)) 40 | 41 | app.get("*", (req, res) => { 42 | res.sendFile(`${__dirname}/dist/index.html`) 43 | }) 44 | 45 | app.listen(port, () => { 46 | console.info(`Sinopia Linked Data Editor running on ${port}`) 47 | console.info("Press Ctrl + C to stop.") 48 | }) 49 | -------------------------------------------------------------------------------- /src/Honeybadger.js: -------------------------------------------------------------------------------- 1 | import Config from "Config" 2 | import { Honeybadger } from "@honeybadger-io/react" 3 | 4 | const HoneybadgerNotifier = Honeybadger.configure({ 5 | apiKey: Config.honeybadgerApiKey, 6 | environment: process.env.SINOPIA_ENV, 7 | revision: Config.honeybadgerRevision, 8 | }) 9 | 10 | export default HoneybadgerNotifier 11 | -------------------------------------------------------------------------------- /src/actionCreators/authenticate.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | import Auth from "@aws-amplify/auth" 3 | 4 | import { setUser, removeUser } from "actions/authenticate" 5 | import { addError, clearErrors } from "actions/errors" 6 | import { hasUser } from "selectors/authenticate" 7 | import { loadUserData } from "actionCreators/user" 8 | 9 | export const authenticate = () => (dispatch, getState) => { 10 | if (hasUser(getState())) return Promise.resolve(true) 11 | return Auth.currentAuthenticatedUser() 12 | .then((cognitoUser) => { 13 | dispatch(setUser(toUser(cognitoUser))) 14 | dispatch(loadUserData(cognitoUser.username)) 15 | return true 16 | }) 17 | .catch(() => { 18 | dispatch(removeUser()) 19 | return false 20 | }) 21 | } 22 | 23 | export const signIn = (username, password, errorKey) => (dispatch) => { 24 | dispatch(clearErrors(errorKey)) 25 | return Auth.signIn(username, password) 26 | .then((cognitoUser) => { 27 | dispatch(setUser(toUser(cognitoUser))) 28 | dispatch(loadUserData(cognitoUser.username)) 29 | }) 30 | .catch((err) => { 31 | dispatch(addError(errorKey, `Login failed: ${err.message}`)) 32 | dispatch(removeUser()) 33 | }) 34 | } 35 | 36 | export const signOut = () => (dispatch) => 37 | Auth.signOut() 38 | .then(() => { 39 | dispatch(removeUser()) 40 | }) 41 | .catch((err) => { 42 | // Not displaying to user as no action user could take. 43 | console.error(err) 44 | }) 45 | 46 | // Note: User model can be extended as we add additional attributes to Cognito. 47 | const toUser = (cognitoUser) => ({ 48 | username: cognitoUser.username, 49 | groups: cognitoUser.signInUserSession.idToken.payload["cognito:groups"] || [], 50 | }) 51 | -------------------------------------------------------------------------------- /src/actionCreators/exports.js: -------------------------------------------------------------------------------- 1 | import Config from "Config" 2 | import { addError, clearErrors } from "actions/errors" 3 | import { exportsReceived } from "actions/exports" 4 | import { hasExports } from "selectors/exports" 5 | 6 | export const fetchExports = (errorKey) => (dispatch, getState) => { 7 | // Return if already loaded. 8 | if (hasExports(getState())) return 9 | 10 | dispatch(clearErrors(errorKey)) 11 | // Not using AWS SDK because requires credentials, which is way too much overhead. 12 | return fetch(Config.exportBucketUrl) 13 | .then((response) => response.text()) 14 | .then((str) => new DOMParser().parseFromString(str, "text/xml")) 15 | .then((data) => { 16 | const elems = data.getElementsByTagName("Key") 17 | const keys = [] 18 | for (let i = 0; i < elems.length; i++) { 19 | keys.push(elems.item(i).innerHTML) 20 | } 21 | dispatch(exportsReceived(keys)) 22 | }) 23 | .catch((err) => 24 | dispatch( 25 | addError( 26 | errorKey, 27 | `Error retrieving list of exports: ${err.message || err}` 28 | ) 29 | ) 30 | ) 31 | } 32 | 33 | export const noop = () => {} 34 | -------------------------------------------------------------------------------- /src/actionCreators/groups.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Stanford University see LICENSE for license 2 | 3 | import { groupsReceived } from "actions/groups" 4 | import { hasGroups } from "selectors/groups" 5 | 6 | import { getGroups } from "sinopiaApi" 7 | 8 | export const fetchGroups = () => (dispatch, getState) => { 9 | if (hasGroups(getState())) { 10 | return // Groups already loaded 11 | } 12 | 13 | return getGroups() 14 | .then((json) => { 15 | dispatch(groupsReceived(json)) 16 | }) 17 | .catch(() => false) 18 | } 19 | 20 | export const noop = () => {} 21 | -------------------------------------------------------------------------------- /src/actionCreators/transfer.js: -------------------------------------------------------------------------------- 1 | import { postTransfer } from "../sinopiaApi" 2 | import { addError } from "actions/errors" 3 | 4 | export const transfer = 5 | (resourceUri, group, target, errorKey) => (dispatch) => { 6 | postTransfer(resourceUri, group, target).catch((err) => { 7 | dispatch( 8 | addError(errorKey, `Error requesting transfer: ${err.message || err}`) 9 | ) 10 | }) 11 | } 12 | 13 | export const noop = () => {} 14 | -------------------------------------------------------------------------------- /src/actionCreators/user.js: -------------------------------------------------------------------------------- 1 | import { fetchUser, putUserHistory } from "sinopiaApi" 2 | import { selectUser } from "selectors/authenticate" 3 | import { 4 | loadTemplateHistory, 5 | loadSearchHistory, 6 | loadResourceHistory, 7 | } from "actionCreators/history" 8 | import md5 from "crypto-js/md5" 9 | 10 | export const loadUserData = (userId) => (dispatch) => 11 | fetchUser(userId) 12 | .then((userData) => { 13 | const templateIds = userData.data.history.template.map( 14 | (historyItem) => historyItem.payload 15 | ) 16 | dispatch(loadTemplateHistory(templateIds)) 17 | const searches = userData.data.history.search.map((historyItem) => 18 | JSON.parse(historyItem.payload) 19 | ) 20 | dispatch(loadSearchHistory(searches)) 21 | const resourceUris = userData.data.history.resource.map( 22 | (historyItem) => historyItem.payload 23 | ) 24 | dispatch(loadResourceHistory(resourceUris)) 25 | }) 26 | .catch((err) => console.error(err)) 27 | 28 | const addHistory = (historyType, payload) => (dispatch, getState) => { 29 | const user = selectUser(getState()) 30 | if (!user) return 31 | return putUserHistory( 32 | user.username, 33 | historyType, 34 | md5(payload).toString(), 35 | payload 36 | ).catch((err) => console.error(err)) 37 | } 38 | 39 | export const addTemplateHistory = (templateId) => (dispatch) => 40 | dispatch(addHistory("template", templateId)) 41 | 42 | export const addResourceHistory = (uri) => (dispatch) => 43 | dispatch(addHistory("resource", uri)) 44 | 45 | export const addSearchHistory = (authorityUri, query) => (dispatch) => { 46 | const payload = JSON.stringify({ authorityUri, query }) 47 | return dispatch(addHistory("search", payload)) 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/authenticate.js: -------------------------------------------------------------------------------- 1 | export const setUser = (user) => ({ 2 | type: "SET_USER", 3 | payload: user, 4 | }) 5 | 6 | export const removeUser = () => ({ 7 | type: "REMOVE_USER", 8 | }) 9 | -------------------------------------------------------------------------------- /src/actions/errors.js: -------------------------------------------------------------------------------- 1 | export const addError = (errorKey, error) => ({ 2 | type: "ADD_ERROR", 3 | payload: { errorKey, error }, 4 | }) 5 | 6 | export const clearErrors = (errorKey) => ({ 7 | type: "CLEAR_ERRORS", 8 | payload: errorKey, 9 | }) 10 | 11 | export const hideValidationErrors = (resourceKey) => ({ 12 | type: "HIDE_VALIDATION_ERRORS", 13 | payload: resourceKey, 14 | }) 15 | 16 | export const showValidationErrors = (resourceKey) => ({ 17 | type: "SHOW_VALIDATION_ERRORS", 18 | payload: resourceKey, 19 | }) 20 | -------------------------------------------------------------------------------- /src/actions/exports.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | export const exportsReceived = (keys) => ({ 3 | type: "EXPORTS_RECEIVED", 4 | payload: keys, 5 | }) 6 | 7 | export const noop = () => {} 8 | -------------------------------------------------------------------------------- /src/actions/groups.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Stanford University see LICENSE for license 2 | 3 | export const groupsReceived = (groups) => ({ 4 | type: "GROUPS_RECEIVED", 5 | payload: groups, 6 | }) 7 | 8 | export const noop = () => {} 9 | -------------------------------------------------------------------------------- /src/actions/history.js: -------------------------------------------------------------------------------- 1 | export const addTemplateHistory = (resourceTemplate) => ({ 2 | type: "ADD_TEMPLATE_HISTORY", 3 | payload: resourceTemplate, 4 | }) 5 | 6 | export const addTemplateHistoryByResult = (result) => ({ 7 | type: "ADD_TEMPLATE_HISTORY_BY_RESULT", 8 | payload: result, 9 | }) 10 | 11 | export const addSearchHistory = (authorityUri, authorityLabel, query) => ({ 12 | type: "ADD_SEARCH_HISTORY", 13 | payload: { authorityUri, authorityLabel, query }, 14 | }) 15 | 16 | export const addResourceHistory = (resourceUri, type, group, modified) => ({ 17 | type: "ADD_RESOURCE_HISTORY", 18 | payload: { 19 | resourceUri, 20 | type, 21 | group, 22 | modified, 23 | }, 24 | }) 25 | 26 | export const addResourceHistoryByResult = (result) => ({ 27 | type: "ADD_RESOURCE_HISTORY_BY_RESULT", 28 | payload: result, 29 | }) 30 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | export const setCurrentComponent = (rootSubjectKey, rootPropertyKey, key) => ({ 3 | type: "SET_CURRENT_COMPONENT", 4 | payload: { rootSubjectKey, rootPropertyKey, key }, 5 | }) 6 | 7 | export const noop = () => {} 8 | -------------------------------------------------------------------------------- /src/actions/languages.js: -------------------------------------------------------------------------------- 1 | export const languageSelected = (valueKey, lang) => ({ 2 | type: "LANGUAGE_SELECTED", 3 | payload: { valueKey, lang }, 4 | }) 5 | 6 | export const languagesReceived = ( 7 | languages, 8 | languageLookup, 9 | scripts, 10 | scriptLookup, 11 | transliterations, 12 | transliterationLookup 13 | ) => ({ 14 | type: "LANGUAGES_RECEIVED", 15 | payload: { 16 | languages, 17 | languageLookup, 18 | scripts, 19 | scriptLookup, 20 | transliterations, 21 | transliterationLookup, 22 | }, 23 | }) 24 | 25 | export const setDefaultLang = (resourceKey, lang) => ({ 26 | type: "SET_DEFAULT_LANG", 27 | payload: { resourceKey, lang }, 28 | }) 29 | -------------------------------------------------------------------------------- /src/actions/lookups.js: -------------------------------------------------------------------------------- 1 | export const lookupOptionsRetrieved = (uri, lookup) => ({ 2 | type: "LOOKUP_OPTIONS_RETRIEVED", 3 | payload: { 4 | uri, 5 | lookup, 6 | }, 7 | }) 8 | 9 | export const noop = () => {} 10 | -------------------------------------------------------------------------------- /src/actions/messages.js: -------------------------------------------------------------------------------- 1 | export const showCopyNewMessage = (oldUri) => ({ 2 | type: "SHOW_COPY_NEW_MESSAGE", 3 | payload: { 4 | oldUri, 5 | timestamp: Date.now(), 6 | }, 7 | }) 8 | 9 | export const noop = () => {} 10 | -------------------------------------------------------------------------------- /src/actions/modals.js: -------------------------------------------------------------------------------- 1 | export const hideModal = () => ({ 2 | type: "HIDE_MODAL", 3 | }) 4 | 5 | export const showModal = (name) => ({ 6 | type: "SHOW_MODAL", 7 | payload: name, 8 | }) 9 | 10 | export const showLangModal = (valueKey) => ({ 11 | type: "SHOW_LANG_MODAL", 12 | payload: valueKey, 13 | }) 14 | 15 | export const showMarcModal = (marc) => ({ 16 | type: "SHOW_MARC_MODAL", 17 | payload: marc, 18 | }) 19 | -------------------------------------------------------------------------------- /src/actions/relationships.js: -------------------------------------------------------------------------------- 1 | export const setRelationships = (resourceKey, relationships) => ({ 2 | type: "SET_RELATIONSHIPS", 3 | payload: { 4 | resourceKey, 5 | relationships, 6 | }, 7 | }) 8 | 9 | export const clearRelationships = (resourceKey) => ({ 10 | type: "CLEAR_RELATIONSHIPS", 11 | payload: resourceKey, 12 | }) 13 | 14 | export const setSearchRelationships = (uri, relationships) => ({ 15 | type: "SET_SEARCH_RELATIONSHIPS", 16 | payload: { 17 | uri, 18 | relationships, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /src/actions/search.js: -------------------------------------------------------------------------------- 1 | export const clearSearchResults = (searchType) => ({ 2 | type: "CLEAR_SEARCH_RESULTS", 3 | payload: searchType, 4 | }) 5 | 6 | export const setSearchResults = ( 7 | searchType, 8 | uri, 9 | results, 10 | totalResults, 11 | facetResults, 12 | query, 13 | options, 14 | error 15 | ) => ({ 16 | type: "SET_SEARCH_RESULTS", 17 | payload: { 18 | searchType, 19 | uri, 20 | results, 21 | totalResults, 22 | facetResults, 23 | query, 24 | options, 25 | error, 26 | }, 27 | }) 28 | 29 | export const setHeaderSearch = (uri, query) => ({ 30 | type: "SET_HEADER_SEARCH", 31 | payload: { 32 | uri, 33 | query, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /src/actions/templates.js: -------------------------------------------------------------------------------- 1 | export const addTemplates = (subjectTemplate) => ({ 2 | type: "ADD_TEMPLATES", 3 | payload: subjectTemplate, 4 | }) 5 | 6 | export const noop = () => {} 7 | -------------------------------------------------------------------------------- /src/components/ClipboardButton.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState, useEffect } from "react" 4 | import PropTypes from "prop-types" 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 6 | import { 7 | faClipboard, 8 | faClipboardCheck, 9 | } from "@fortawesome/free-solid-svg-icons" 10 | 11 | const ClipboardButton = ({ text, label = null }) => { 12 | const [isCopying, setCopying] = useState(false) 13 | const [timerId, setTimerId] = useState(false) 14 | 15 | useEffect(() => () => { 16 | if (timerId) clearTimeout(timerId) 17 | }) 18 | 19 | const handleClick = (event) => { 20 | navigator.clipboard.writeText(text) 21 | setCopying(true) 22 | setTimerId(setTimeout(() => setCopying(false), 1000)) 23 | event.preventDefault() 24 | } 25 | 26 | if (!text) { 27 | return null 28 | } 29 | 30 | if (isCopying) 31 | return ( 32 | 41 | ) 42 | 43 | return ( 44 | 53 | ) 54 | } 55 | 56 | ClipboardButton.propTypes = { 57 | text: PropTypes.string, 58 | label: PropTypes.string.isRequired, 59 | } 60 | 61 | export default ClipboardButton 62 | -------------------------------------------------------------------------------- /src/components/LongDate.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | 6 | const LongDate = (props) => { 7 | const date = new Date(props.datetime) 8 | if (!props.datetime || !date || !date.getTime || Number.isNaN(date.getTime())) 9 | return null 10 | const options = { 11 | month: "short", 12 | day: "numeric", 13 | year: "numeric", 14 | timeZone: props.timeZone, 15 | } 16 | const long = date.toLocaleString("default", options) 17 | return ( 18 | 21 | ) 22 | } 23 | 24 | LongDate.propTypes = { 25 | datetime: PropTypes.string, 26 | timeZone: PropTypes.string, 27 | } 28 | export default LongDate 29 | -------------------------------------------------------------------------------- /src/components/alerts/Alert.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useRef, useState, useLayoutEffect } from "react" 4 | import PropTypes from "prop-types" 5 | import AlertWrapper from "./AlertWrapper" 6 | import _ from "lodash" 7 | 8 | const Alert = ({ errors }) => { 9 | const ref = useRef() 10 | const [lastErrors, setLastErrors] = useState(false) 11 | 12 | useLayoutEffect(() => { 13 | // Only scroll if changed errors 14 | if (_.isEqual(lastErrors, errors)) return 15 | if (!_.isEmpty(errors)) window.scrollTo(0, ref.current.offsetTop) 16 | setLastErrors([...errors]) 17 | }, [errors, lastErrors]) 18 | 19 | if (_.isEmpty(errors)) return null 20 | 21 | const errorText = errors.map((error) =>

{error}

) 22 | 23 | return {errorText} 24 | } 25 | 26 | Alert.propTypes = { 27 | errors: PropTypes.array.isRequired, 28 | } 29 | 30 | export default Alert 31 | -------------------------------------------------------------------------------- /src/components/alerts/AlertWrapper.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { forwardRef } from "react" 4 | import PropTypes from "prop-types" 5 | 6 | const AlertWrapper = forwardRef(({ children }, ref) => ( 7 |
8 |
9 |
10 | {children} 11 |
12 |
13 |
14 | )) 15 | AlertWrapper.displayName = "AlertWrapper" 16 | 17 | AlertWrapper.propTypes = { 18 | children: PropTypes.oneOfType([ 19 | PropTypes.node, 20 | PropTypes.arrayOf(PropTypes.node), 21 | ]), 22 | } 23 | 24 | export default AlertWrapper 25 | -------------------------------------------------------------------------------- /src/components/alerts/AlertsContextProvider.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | 4 | export const AlertsContext = React.createContext() 5 | 6 | const AlertsContextProvider = ({ value, children }) => ( 7 | {children} 8 | ) 9 | 10 | AlertsContextProvider.propTypes = { 11 | value: PropTypes.string.isRequired, 12 | children: PropTypes.oneOfType([ 13 | PropTypes.node, 14 | PropTypes.arrayOf(PropTypes.node), 15 | ]), 16 | } 17 | 18 | export default AlertsContextProvider 19 | -------------------------------------------------------------------------------- /src/components/alerts/ContextAlert.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useEffect } from "react" 4 | import { useDispatch, useSelector } from "react-redux" 5 | import { selectErrors } from "selectors/errors" 6 | import useAlerts from "hooks/useAlerts" 7 | import Alert from "./Alert" 8 | import { hideModal } from "actions/modals" 9 | import _ from "lodash" 10 | 11 | const ContextAlert = () => { 12 | const dispatch = useDispatch() 13 | const errorKey = useAlerts() 14 | const errors = useSelector((state) => selectErrors(state, errorKey)) 15 | 16 | useEffect(() => { 17 | if (!_.isEmpty(errors)) dispatch(hideModal()) 18 | }, [errors, dispatch]) 19 | 20 | if (_.isEmpty(errors)) return null 21 | 22 | return 23 | } 24 | 25 | export default ContextAlert 26 | -------------------------------------------------------------------------------- /src/components/buttons/CopyButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 4 | import { faCopy } from "@fortawesome/free-solid-svg-icons" 5 | import LoadingButton from "./LoadingButton" 6 | 7 | const CopyButton = ({ label, handleClick, isLoading = false, size = "lg" }) => { 8 | if (isLoading) return 9 | 10 | return ( 11 | 20 | ) 21 | } 22 | 23 | CopyButton.propTypes = { 24 | label: PropTypes.string.isRequired, 25 | handleClick: PropTypes.func.isRequired, 26 | isLoading: PropTypes.bool, 27 | size: PropTypes.string, 28 | } 29 | 30 | export default CopyButton 31 | -------------------------------------------------------------------------------- /src/components/buttons/EditButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 4 | import { faPencilAlt } from "@fortawesome/free-solid-svg-icons" 5 | import LoadingButton from "./LoadingButton" 6 | 7 | const EditButton = ({ label, handleClick, isLoading = false, size = "lg" }) => { 8 | if (isLoading) return 9 | 10 | return ( 11 | 20 | ) 21 | } 22 | 23 | EditButton.propTypes = { 24 | label: PropTypes.string.isRequired, 25 | handleClick: PropTypes.func.isRequired, 26 | isLoading: PropTypes.bool, 27 | size: PropTypes.string, 28 | } 29 | 30 | export default EditButton 31 | -------------------------------------------------------------------------------- /src/components/buttons/LoadingButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const LoadingButton = () => ( 4 | 11 | ) 12 | 13 | export default LoadingButton 14 | -------------------------------------------------------------------------------- /src/components/buttons/NewButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { FileEarmarkPlusFill } from "react-bootstrap-icons" 4 | import LoadingButton from "./LoadingButton" 5 | 6 | const sizeMap = { 7 | lg: 32, 8 | } 9 | 10 | const NewButton = ({ label, handleClick, isLoading = false, size = "lg" }) => { 11 | if (isLoading) return 12 | 13 | const sizeValue = sizeMap[size] 14 | if (!sizeValue) console.error("Unknown size", size) 15 | 16 | return ( 17 | 26 | ) 27 | } 28 | 29 | NewButton.propTypes = { 30 | label: PropTypes.string.isRequired, 31 | handleClick: PropTypes.func.isRequired, 32 | isLoading: PropTypes.bool, 33 | size: PropTypes.string, 34 | } 35 | 36 | export default NewButton 37 | -------------------------------------------------------------------------------- /src/components/buttons/ViewButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 4 | import { faEye } from "@fortawesome/free-solid-svg-icons" 5 | import LoadingButton from "./LoadingButton" 6 | 7 | const ViewButton = ({ label, handleClick, isLoading = false, size = "lg" }) => { 8 | if (isLoading) return 9 | 10 | return ( 11 | 20 | ) 21 | } 22 | 23 | ViewButton.propTypes = { 24 | label: PropTypes.string.isRequired, 25 | handleClick: PropTypes.func.isRequired, 26 | isLoading: PropTypes.bool, 27 | size: PropTypes.string, 28 | } 29 | 30 | export default ViewButton 31 | -------------------------------------------------------------------------------- /src/components/dashboard/ResourceList.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | /* eslint max-params: ["error", 4] */ 3 | 4 | import React from "react" 5 | import PropTypes from "prop-types" 6 | import SearchResultRows from "../search/SearchResultRows" 7 | 8 | const ResourceList = (props) => { 9 | if (props.resources.length === 0) { 10 | return null 11 | } 12 | 13 | return ( 14 | 15 |
16 |
17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Label / IDClassGroupModified
34 |
35 |
36 |
37 | ) 38 | } 39 | 40 | ResourceList.propTypes = { 41 | resources: PropTypes.array.isRequired, 42 | } 43 | 44 | export default ResourceList 45 | -------------------------------------------------------------------------------- /src/components/dashboard/SearchRow.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 6 | import { faSearch } from "@fortawesome/free-solid-svg-icons" 7 | 8 | const SearchRow = (props) => ( 9 | 10 | 11 | {props.row.authorityLabel} 12 | 13 | {props.row.query} 14 | 15 |
16 | 28 |
29 | 30 | 31 | ) 32 | 33 | SearchRow.propTypes = { 34 | row: PropTypes.object.isRequired, 35 | handleSearch: PropTypes.func.isRequired, 36 | } 37 | 38 | export default SearchRow 39 | -------------------------------------------------------------------------------- /src/components/editor/CopyToNewMessage.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import ExpiringMessage from "./ExpiringMessage" 6 | import { 7 | selectCopyToNewMessageOldUri, 8 | selectCopyToNewMessageTimestamp, 9 | } from "selectors/messages" 10 | 11 | const CopyToNewMessage = () => { 12 | const oldUri = useSelector((state) => selectCopyToNewMessageOldUri(state)) 13 | const timestamp = useSelector((state) => 14 | selectCopyToNewMessageTimestamp(state) 15 | ) 16 | 17 | return ( 18 | 19 | Copied {oldUri} to new resource. 20 | 21 | ) 22 | } 23 | 24 | export default CopyToNewMessage 25 | -------------------------------------------------------------------------------- /src/components/editor/EditorActions.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import CloseButton from "./actions/CloseButton" 6 | import SaveAndPublishButton from "./actions/SaveAndPublishButton" 7 | import MarcButton from "./actions/MarcButton" 8 | import TransferButtons from "./actions/TransferButtons" 9 | import { selectCurrentResourceKey } from "selectors/resources" 10 | 11 | // CopyToNewButton and PreviewButton are now called from ResourceComponent 12 | const EditorActions = () => { 13 | const currentResourceKey = useSelector((state) => 14 | selectCurrentResourceKey(state) 15 | ) 16 | 17 | return ( 18 |
19 |
20 | 21 | 22 | 23 | 24 |
25 |
26 | ) 27 | } 28 | 29 | export default EditorActions 30 | -------------------------------------------------------------------------------- /src/components/editor/ErrorMessages.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import PropTypes from "prop-types" 6 | import { selectValidationErrors } from "selectors/errors" 7 | import AlertWrapper from "components/alerts/AlertWrapper" 8 | import _ from "lodash" 9 | 10 | const ErrorMessages = ({ resourceKey }) => { 11 | // To determine if errors have changed, check length first and then isEqual. 12 | // Most changes in errors will change the length, but not all. 13 | const errors = useSelector( 14 | (state) => selectValidationErrors(state, resourceKey), 15 | (obj1, obj2) => obj1?.length === obj2?.length && _.isEqual(obj1, obj2) 16 | ) 17 | if (_.isEmpty(errors)) return null 18 | 19 | const errorList = errors.map((error) => ( 20 |
  • 21 | {error.labelPath.join(" > ")}: {error.message} 22 |
  • 23 | )) 24 | const text = ( 25 | 26 | Unable to save this resource. Validation errors:
      {errorList}
    27 |
    28 | ) 29 | return {text} 30 | } 31 | 32 | ErrorMessages.propTypes = { 33 | resourceKey: PropTypes.string.isRequired, 34 | } 35 | 36 | export default ErrorMessages 37 | -------------------------------------------------------------------------------- /src/components/editor/ExpiringMessage.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState, useEffect, useLayoutEffect, useRef } from "react" 4 | import PropTypes from "prop-types" 5 | 6 | const ExpiringMessage = ({ timestamp, children, scroll = true }) => { 7 | const [prevLastSave, setPrevLastSave] = useState(timestamp) 8 | const inputRef = useRef(null) 9 | 10 | useEffect( 11 | () => 12 | function cleanup() { 13 | if (timer !== undefined) { 14 | clearInterval(timer) 15 | } 16 | } 17 | ) 18 | 19 | useLayoutEffect(() => { 20 | if (!scroll || !timestamp) return 21 | inputRef.current?.scrollIntoView({ 22 | behavior: "smooth", 23 | block: "end", 24 | }) 25 | }, [scroll, timestamp]) 26 | 27 | if (!timestamp || prevLastSave === timestamp) { 28 | return null 29 | } 30 | 31 | const timer = setInterval(() => setPrevLastSave(timestamp), 3000) 32 | 33 | return ( 34 |
    35 | {children} 36 |
    37 | ) 38 | } 39 | 40 | ExpiringMessage.propTypes = { 41 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired, 42 | timestamp: PropTypes.number, 43 | scroll: PropTypes.bool, 44 | } 45 | 46 | export default ExpiringMessage 47 | -------------------------------------------------------------------------------- /src/components/editor/ResourceTitle.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import { isBfWork, isBfInstance, isBfItem } from "utilities/Bibframe" 6 | 7 | /** 8 | * Shows the resources title 9 | */ 10 | const ResourceTitle = ({ resource }) => { 11 | let badge = null 12 | if (isBfWork(resource.classes)) badge = "WORK" 13 | if (isBfInstance(resource.classes)) badge = "INSTANCE" 14 | if (isBfItem(resource.classes)) badge = "ITEM" 15 | 16 | return ( 17 | 18 | {resource.label} 19 | {badge && {badge}} 20 | 21 | ) 22 | } 23 | 24 | ResourceTitle.propTypes = { 25 | resource: PropTypes.object.isRequired, 26 | } 27 | 28 | export default ResourceTitle 29 | -------------------------------------------------------------------------------- /src/components/editor/ResourceURIMessage.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import PropTypes from "prop-types" 6 | import { selectUri } from "selectors/resources" 7 | import ClipboardButton from "../ClipboardButton" 8 | 9 | // Renders the resource URI message for saved resource 10 | const ResourceURIMessage = ({ resourceKey }) => { 11 | const uri = useSelector((state) => selectUri(state, resourceKey)) 12 | 13 | if (!uri) { 14 | return null 15 | } 16 | 17 | return ( 18 |

    19 | URI for this resource: <{uri}>  20 | 21 |

    22 | ) 23 | } 24 | 25 | ResourceURIMessage.propTypes = { 26 | resourceKey: PropTypes.string.isRequired, 27 | } 28 | 29 | export default ResourceURIMessage 30 | -------------------------------------------------------------------------------- /src/components/editor/ResourcesNav.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import { 6 | selectCurrentResourceKey, 7 | selectResourceKeys, 8 | } from "selectors/resources" 9 | import ResourcesNavTab from "./ResourcesNavTab" 10 | 11 | const ResourcesNav = () => { 12 | const currentResourceKey = useSelector((state) => 13 | selectCurrentResourceKey(state) 14 | ) 15 | const resourceKeys = useSelector((state) => selectResourceKeys(state)) 16 | 17 | const navTabs = resourceKeys.map((resourceKey) => ( 18 | 23 | )) 24 | 25 | if (resourceKeys.length === 1) return null 26 | 27 | return ( 28 |
    29 |
    30 |
      {navTabs}
    31 |
    32 |
    33 | ) 34 | } 35 | 36 | export default ResourcesNav 37 | -------------------------------------------------------------------------------- /src/components/editor/ResourcesNavTab.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector, useDispatch } from "react-redux" 5 | import PropTypes from "prop-types" 6 | import CloseButton from "./actions/CloseButton" 7 | import { selectPickSubject } from "selectors/resources" 8 | import { setCurrentResource } from "actions/resources" 9 | import ResourceTitle from "./ResourceTitle" 10 | 11 | const ResourcesNavTab = ({ resourceKey, active }) => { 12 | const dispatch = useDispatch() 13 | 14 | const resource = useSelector((state) => 15 | selectPickSubject(state, resourceKey, ["label", "classes"]) 16 | ) 17 | 18 | const handleResourceNavClick = (event) => { 19 | event.preventDefault() 20 | dispatch(setCurrentResource(resourceKey)) 21 | } 22 | 23 | const itemClasses = ["nav-item"] 24 | let closeButton 25 | if (active) { 26 | itemClasses.push("active") 27 | } else { 28 | closeButton = ( 29 | 33 | ) 34 | } 35 | 36 | return ( 37 |
  • 38 | 43 | 44 | 45 | 46 | {closeButton} 47 |
  • 48 | ) 49 | } 50 | 51 | ResourcesNavTab.propTypes = { 52 | resourceKey: PropTypes.string.isRequired, 53 | active: PropTypes.bool.isRequired, 54 | } 55 | 56 | export default ResourcesNavTab 57 | -------------------------------------------------------------------------------- /src/components/editor/SaveAlert.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import ExpiringMessage from "./ExpiringMessage" 6 | import { selectCurrentResourceKey, selectLastSave } from "selectors/resources" 7 | 8 | const SaveAlert = () => { 9 | const resourceKey = useSelector((state) => selectCurrentResourceKey(state)) 10 | const lastSave = useSelector((state) => selectLastSave(state, resourceKey)) 11 | 12 | return ( 13 | 14 | Saved 15 | 16 | ) 17 | } 18 | 19 | export default SaveAlert 20 | -------------------------------------------------------------------------------- /src/components/editor/ToggleButton.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 6 | import { faAngleRight, faAngleDown } from "@fortawesome/free-solid-svg-icons" 7 | 8 | const ToggleButton = ({ 9 | handleClick, 10 | isExpanded, 11 | label, 12 | isDisabled = false, 13 | }) => ( 14 | 28 | ) 29 | 30 | ToggleButton.propTypes = { 31 | handleClick: PropTypes.func.isRequired, 32 | isExpanded: PropTypes.bool.isRequired, 33 | label: PropTypes.string.isRequired, 34 | isDisabled: PropTypes.bool, 35 | } 36 | 37 | export default ToggleButton 38 | -------------------------------------------------------------------------------- /src/components/editor/actions/CopyToNewButton.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useDispatch, useSelector } from "react-redux" 5 | import PropTypes from "prop-types" 6 | import { showCopyNewMessage } from "actions/messages" 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 8 | import { faCopy } from "@fortawesome/free-solid-svg-icons" 9 | import { 10 | selectCurrentResourceKey, 11 | selectNormSubject, 12 | } from "selectors/resources" 13 | import { newResourceCopy } from "actionCreators/resources" 14 | 15 | const CopyToNewButton = (props) => { 16 | const dispatch = useDispatch() 17 | const resourceKey = useSelector((state) => selectCurrentResourceKey(state)) 18 | const resource = useSelector((state) => selectNormSubject(state, resourceKey)) 19 | 20 | const handleClick = () => { 21 | dispatch(newResourceCopy(resource.key)) 22 | dispatch(showCopyNewMessage(resource.uri)) 23 | } 24 | 25 | return ( 26 | 38 | ) 39 | } 40 | 41 | CopyToNewButton.propTypes = { 42 | copyResourceToEditor: PropTypes.func, 43 | id: PropTypes.string, 44 | } 45 | 46 | export default CopyToNewButton 47 | -------------------------------------------------------------------------------- /src/components/editor/actions/MarcModal.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useSelector } from "react-redux" 3 | import { selectMarc } from "selectors/modals" 4 | import ModalWrapper from "../../ModalWrapper" 5 | import ClipboardButton from "../../ClipboardButton" 6 | 7 | const MarcModal = () => { 8 | const marc = useSelector((state) => selectMarc(state)) 9 | 10 | const body = ( 11 | 12 |
    13 | 14 |
    15 |
    16 |         {marc}
    17 |       
    18 |
    19 | ) 20 | 21 | return ( 22 | 28 | ) 29 | } 30 | 31 | export default MarcModal 32 | -------------------------------------------------------------------------------- /src/components/editor/actions/PermissionsAction.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector, useDispatch } from "react-redux" 5 | import { selectCurrentResourceKey, selectUri } from "selectors/resources" 6 | import { showModal as showModalAction } from "actions/modals" 7 | import { 8 | displayResourceValidations, 9 | hasValidationErrors as hasValidationErrorsSelector, 10 | } from "selectors/errors" 11 | 12 | // Renders the permissions link for saved resource 13 | const PermissionsAction = () => { 14 | const resourceKey = useSelector((state) => selectCurrentResourceKey(state)) 15 | const uri = useSelector((state) => selectUri(state, resourceKey)) 16 | 17 | const hasValidationErrors = useSelector((state) => 18 | hasValidationErrorsSelector(state, resourceKey) 19 | ) 20 | const validationErrorsAreShowing = useSelector((state) => 21 | displayResourceValidations(state, resourceKey) 22 | ) 23 | 24 | const dispatch = useDispatch() 25 | const showGroupChooser = () => dispatch(showModalAction("GroupChoiceModal")) 26 | 27 | const handleClick = (event) => { 28 | showGroupChooser() 29 | event.preventDefault() 30 | } 31 | 32 | if (!uri) return null 33 | 34 | if (validationErrorsAreShowing && hasValidationErrors) return null 35 | 36 | return ( 37 | 44 | ) 45 | } 46 | 47 | export default PermissionsAction 48 | -------------------------------------------------------------------------------- /src/components/editor/actions/PreviewButton.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 5 | import { faEye } from "@fortawesome/free-solid-svg-icons" 6 | import { useDispatch } from "react-redux" 7 | import { showModal } from "actions/modals" 8 | 9 | const PreviewButton = () => { 10 | const dispatch = useDispatch() 11 | 12 | const handleClick = (event) => { 13 | dispatch(showModal("RDFModal")) 14 | event.preventDefault() 15 | } 16 | 17 | return ( 18 | 27 | ) 28 | } 29 | 30 | export default PreviewButton 31 | -------------------------------------------------------------------------------- /src/components/editor/actions/TopButton.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 5 | import { faArrowAltCircleUp } from "@fortawesome/free-solid-svg-icons" 6 | 7 | const TopButton = () => { 8 | const handleClick = (event) => { 9 | window.scrollTo(0, 0) 10 | event.preventDefault() 11 | } 12 | 13 | return ( 14 | 23 | ) 24 | } 25 | 26 | export default TopButton 27 | -------------------------------------------------------------------------------- /src/components/editor/actions/TransferButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | import PropTypes from "prop-types" 3 | 4 | const TransferButton = ({ label, handleClick }) => { 5 | const [btnText, setBtnText] = useState(label) 6 | const timerRef = useRef(null) 7 | 8 | useEffect( 9 | () => () => { 10 | if (timerRef.current) clearTimeout(timerRef.current) 11 | }, 12 | [] 13 | ) 14 | 15 | const handleBtnClick = (event) => { 16 | setBtnText(Requesting) 17 | timerRef.current = setTimeout(() => setBtnText(label), 3000) 18 | handleClick(event) 19 | event.preventDefault() 20 | } 21 | 22 | return ( 23 | 30 | ) 31 | } 32 | 33 | TransferButton.propTypes = { 34 | label: PropTypes.string.isRequired, 35 | handleClick: PropTypes.func.isRequired, 36 | } 37 | 38 | export default TransferButton 39 | -------------------------------------------------------------------------------- /src/components/editor/diacritics/CharacterButton.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | 6 | const CharacterButton = (props) => { 7 | const cleanCharacter = () => { 8 | // For some reason, some combining characters are precombined with ◌ (U+25CC) 9 | let cleanChars = "" 10 | if (props.character.length > 1) { 11 | for (let i = 0; i < props.character.length; i++) { 12 | if (props.character.codePointAt(i) !== 9676) { 13 | cleanChars += props.character[i] 14 | } 15 | } 16 | } else { 17 | cleanChars = props.character 18 | } 19 | return cleanChars 20 | } 21 | 22 | const handleClick = (event) => { 23 | props.handleAddCharacter(cleanCharacter()) 24 | event.preventDefault() 25 | } 26 | 27 | return ( 28 | 35 | ) 36 | } 37 | 38 | CharacterButton.propTypes = { 39 | character: PropTypes.string.isRequired, 40 | handleAddCharacter: PropTypes.func.isRequired, 41 | } 42 | 43 | export default CharacterButton 44 | -------------------------------------------------------------------------------- /src/components/editor/diacritics/VocabChoice.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import specialcharacters from "../../../../static/specialcharacters.json" 6 | 7 | const VocabChoice = (props) => { 8 | const getOptions = () => { 9 | const options = [] 10 | Object.keys(specialcharacters).map((key) => { 11 | options.push( 12 | 15 | ) 16 | }) 17 | return options 18 | } 19 | 20 | const handleChange = (event) => { 21 | props.selectVocabulary(event) 22 | } 23 | 24 | return ( 25 | 37 | ) 38 | } 39 | 40 | VocabChoice.propTypes = { 41 | selectVocabulary: PropTypes.func.isRequired, 42 | vocabulary: PropTypes.string.isRequired, 43 | } 44 | 45 | export default VocabChoice 46 | -------------------------------------------------------------------------------- /src/components/editor/inputs/DiacriticsButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | 4 | const DiacriticsButton = ({ id, content, handleClick, handleBlur }) => ( 5 | 15 | ) 16 | 17 | DiacriticsButton.propTypes = { 18 | id: PropTypes.string.isRequired, 19 | content: PropTypes.string.isRequired, 20 | handleClick: PropTypes.func.isRequired, 21 | handleBlur: PropTypes.func.isRequired, 22 | } 23 | 24 | export default DiacriticsButton 25 | -------------------------------------------------------------------------------- /src/components/editor/inputs/LanguageButton.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import { useDispatch, useSelector } from "react-redux" 6 | import { showLangModal } from "actions/modals" 7 | import { selectLanguageLabel } from "selectors/languages" 8 | 9 | const LanguageButton = ({ value }) => { 10 | const dispatch = useDispatch() 11 | const langLabel = useSelector((state) => 12 | selectLanguageLabel(state, value.lang) 13 | ) 14 | 15 | const handleClick = (event) => { 16 | event.preventDefault() 17 | dispatch(showLangModal(value.key)) 18 | } 19 | 20 | const label = `Change language for ${value.literal || value.label || ""}` 21 | 22 | return ( 23 | 24 | 34 | 35 | ) 36 | } 37 | 38 | LanguageButton.propTypes = { 39 | value: PropTypes.object.isRequired, 40 | } 41 | 42 | export default LanguageButton 43 | -------------------------------------------------------------------------------- /src/components/editor/inputs/ReadOnlyInputLiteralOrURI.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useSelector } from "react-redux" 3 | import PropTypes from "prop-types" 4 | import { selectProperty } from "selectors/resources" 5 | import { isHttp } from "utilities/Utilities" 6 | import _ from "lodash" 7 | 8 | const ReadOnlyInputLiteralOrURI = ({ propertyKey }) => { 9 | const property = useSelector((state) => selectProperty(state, propertyKey)) 10 | 11 | const filteredValues = property.values.filter( 12 | (value) => value.literal || value.uri 13 | ) 14 | 15 | if (_.isEmpty(filteredValues)) return null 16 | 17 | const uriValue = (value) => { 18 | const uri = isHttp(value.uri) ? ( 19 | 20 | {value.uri} 21 | 22 | ) : ( 23 | value.uri 24 | ) 25 | if (value.label) { 26 | const langLabel = value.lang ? ` [${value.lang}]` : "" 27 | return ( 28 |

    29 | {value.label} 30 | {langLabel}: {uri} 31 |

    32 | ) 33 | } 34 | return

    {uri}

    35 | } 36 | 37 | const literalValue = (value) => { 38 | const language = value.lang || "No language specified" 39 | return ( 40 |

    41 | {value.literal} [{language}] 42 |

    43 | ) 44 | } 45 | 46 | const inputValues = filteredValues.map((value) => 47 | value.component === "InputLiteralValue" 48 | ? literalValue(value) 49 | : uriValue(value) 50 | ) 51 | 52 | return {inputValues} 53 | } 54 | 55 | ReadOnlyInputLiteralOrURI.propTypes = { 56 | propertyKey: PropTypes.string.isRequired, 57 | } 58 | 59 | export default ReadOnlyInputLiteralOrURI 60 | -------------------------------------------------------------------------------- /src/components/editor/inputs/RemoveButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 4 | import { faTrashAlt } from "@fortawesome/free-solid-svg-icons" 5 | 6 | const RemoveButton = ({ content, handleClick }) => ( 7 | 16 | ) 17 | 18 | RemoveButton.propTypes = { 19 | content: PropTypes.string.isRequired, 20 | handleClick: PropTypes.func.isRequired, 21 | } 22 | 23 | export default RemoveButton 24 | -------------------------------------------------------------------------------- /src/components/editor/leftNav/PanelResourceNav.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import ActivePanelPropertyNav from "./ActivePanelPropertyNav" 6 | 7 | const PanelResourceNav = (props) => { 8 | const isTemplate = 9 | props.resource.subjectTemplateKey === "sinopia:template:resource" 10 | const classNames = ["resource-nav-list-group"] 11 | if (isTemplate) { 12 | classNames.push("template") 13 | } 14 | 15 | const navItems = props.resource.propertyKeys.map((propertyKey) => ( 16 | 21 | )) 22 | return ( 23 |
    24 |
      {navItems}
    25 |
    26 | ) 27 | } 28 | 29 | PanelResourceNav.propTypes = { 30 | resource: PropTypes.object.isRequired, 31 | } 32 | 33 | export default PanelResourceNav 34 | -------------------------------------------------------------------------------- /src/components/editor/leftNav/PresenceIndicator.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 4 | import { faCircle } from "@fortawesome/free-solid-svg-icons" 5 | import _ from "lodash" 6 | 7 | const PresenceIndicator = (props) => { 8 | if (_.isEmpty(props.valueKeys)) return null 9 | 10 | return ( 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | PresenceIndicator.propTypes = { 18 | valueKeys: PropTypes.array.isRequired, 19 | } 20 | 21 | export default PresenceIndicator 22 | -------------------------------------------------------------------------------- /src/components/editor/leftNav/Relationships.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import RelationshipsDisplay from "./RelationshipsDisplay" 6 | 7 | const Relationships = ({ resourceKey }) => ( 8 |
    9 |
    10 | 11 |
    12 |
    13 | ) 14 | 15 | Relationships.propTypes = { 16 | resourceKey: PropTypes.string.isRequired, 17 | } 18 | 19 | export default Relationships 20 | -------------------------------------------------------------------------------- /src/components/editor/preview/EditorPreviewModal.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import { isCurrentModal } from "selectors/modals" 6 | import ModalWrapper from "../../ModalWrapper" 7 | import SaveAndPublishButton from "../actions/SaveAndPublishButton" 8 | import ResourceDisplay from "./ResourceDisplay" 9 | import { selectCurrentResourceKey } from "selectors/resources" 10 | 11 | const EditorPreviewModal = () => { 12 | const show = useSelector((state) => isCurrentModal(state, "RDFModal")) 13 | const resourceKey = useSelector((state) => selectCurrentResourceKey(state)) 14 | 15 | const header =

    Preview

    16 | 17 | const body = show ? ( 18 | 23 | ) : null 24 | 25 | const footer = 26 | 27 | return ( 28 | 37 | ) 38 | } 39 | 40 | export default EditorPreviewModal 41 | -------------------------------------------------------------------------------- /src/components/editor/property/LiteralTypeLabel.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | 4 | const LiteralTypeLabel = ({ propertyTemplate }) => { 5 | const labelText = (propertyTemplate) => { 6 | const typeLabelLookup = { 7 | "http://www.w3.org/2001/XMLSchema#integer": "an integer", 8 | "http://www.w3.org/2001/XMLSchema#dateTime": "a date time", 9 | "http://www.w3.org/2001/XMLSchema#dateTimeStamp": 10 | "a date time with timezone", 11 | "http://id.loc.gov/datatypes/edtf": 12 | "an Extended Date Time Format (EDTF) date", 13 | } 14 | 15 | let label = `Enter ${ 16 | typeLabelLookup[propertyTemplate.validationDataType] ?? "a literal" 17 | }` 18 | if (propertyTemplate.validationRegex) { 19 | label += ` in the form "${propertyTemplate.validationRegex}"` 20 | } 21 | 22 | return label 23 | } 24 | 25 | return ( 26 |
    27 |
    {labelText(propertyTemplate)}
    28 |
    29 | ) 30 | } 31 | 32 | LiteralTypeLabel.propTypes = { 33 | propertyTemplate: PropTypes.object.isRequired, 34 | } 35 | 36 | export default LiteralTypeLabel 37 | -------------------------------------------------------------------------------- /src/components/editor/property/PanelResource.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import PanelProperty from "./PanelProperty" 6 | import LeftNav from "../leftNav/LeftNav" 7 | import ResourceClass from "./ResourceClass" 8 | 9 | // Top-level resource 10 | const PanelResource = ({ resource, readOnly = false }) => { 11 | const resourceDivClass = readOnly ? "col-md-12" : "col-md-7 col-lg-8 col-xl-9" 12 | const isTemplate = resource.subjectTemplateKey === "sinopia:template:resource" 13 | 14 | return ( 15 |
    16 | {!readOnly && } 17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 |
    24 | {resource.propertyKeys.map((propertyKey, index) => ( 25 | 33 | ))} 34 | 35 |
    36 |
    37 | ) 38 | } 39 | 40 | PanelResource.propTypes = { 41 | resource: PropTypes.object.isRequired, 42 | readOnly: PropTypes.bool, 43 | } 44 | 45 | export default PanelResource 46 | -------------------------------------------------------------------------------- /src/components/editor/property/PropertyLabel.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import RequiredSuperscript from "./RequiredSuperscript" 6 | 7 | const PropertyLabel = ({ label, required }) => ( 8 | 9 | {label} 10 | {required && } 11 | 12 | ) 13 | 14 | PropertyLabel.propTypes = { 15 | label: PropTypes.string.isRequired, 16 | required: PropTypes.bool.isRequired, 17 | } 18 | 19 | export default PropertyLabel 20 | -------------------------------------------------------------------------------- /src/components/editor/property/PropertyLabelInfo.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import PropertyLabelInfoLink from "./PropertyLabelInfoLink" 6 | import PropertyLabelInfoTooltip from "./PropertyLabelInfoTooltip" 7 | 8 | import _ from "lodash" 9 | 10 | const PropertyLabelInfo = ({ propertyTemplate }) => ( 11 | 12 | {!_.isEmpty(propertyTemplate.remark) && ( 13 | 14 | )} 15 | {!_.isEmpty(propertyTemplate.remarkUrl) && ( 16 | 17 | )} 18 | 19 | ) 20 | 21 | PropertyLabelInfo.propTypes = { 22 | propertyTemplate: PropTypes.object.isRequired, 23 | } 24 | export default PropertyLabelInfo 25 | -------------------------------------------------------------------------------- /src/components/editor/property/PropertyLabelInfoLink.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 6 | import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons" 7 | 8 | const PropertyLabelInfoLink = (props) => { 9 | const url = new URL(props.propertyTemplate.remarkUrl) 10 | 11 | return ( 12 | 22 | 23 | 24 | ) 25 | } 26 | 27 | PropertyLabelInfoLink.propTypes = { 28 | propertyTemplate: PropTypes.object.isRequired, 29 | } 30 | export default PropertyLabelInfoLink 31 | -------------------------------------------------------------------------------- /src/components/editor/property/PropertyLabelInfoTooltip.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useEffect, useRef } from "react" 4 | import PropTypes from "prop-types" 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 6 | import { faInfoCircle } from "@fortawesome/free-solid-svg-icons" 7 | import { Popover } from "bootstrap" 8 | 9 | const PropertyLabelInfoTooltip = (props) => { 10 | const popoverRef = useRef() 11 | 12 | useEffect(() => { 13 | const popover = new Popover(popoverRef.current) 14 | 15 | return () => popover.hide 16 | }, [popoverRef]) 17 | 18 | const linkedRemarks = (remark) => { 19 | const urlRegex = 20 | /(\b(https?):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi 21 | return remark.replace( 22 | urlRegex, 23 | (match) => `${match}` 24 | ) 25 | } 26 | 27 | return ( 28 | 42 | 43 | 44 | ) 45 | } 46 | 47 | PropertyLabelInfoTooltip.propTypes = { 48 | propertyTemplate: PropTypes.object.isRequired, 49 | } 50 | export default PropertyLabelInfoTooltip 51 | -------------------------------------------------------------------------------- /src/components/editor/property/PropertyPropertyURI.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { setPropertyPropertyURI } from "actions/resources" 4 | import PropertyURI from "./PropertyURI" 5 | 6 | const PropertyPropertyURI = ({ 7 | propertyTemplate, 8 | property, 9 | readOnly = false, 10 | }) => { 11 | if (!propertyTemplate.ordered) return null 12 | 13 | return ( 14 | 20 | ) 21 | } 22 | 23 | PropertyPropertyURI.propTypes = { 24 | propertyTemplate: PropTypes.object.isRequired, 25 | property: PropTypes.object.isRequired, 26 | readOnly: PropTypes.bool, 27 | } 28 | 29 | export default PropertyPropertyURI 30 | -------------------------------------------------------------------------------- /src/components/editor/property/RequiredSuperscript.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import { nanoid } from "nanoid" 4 | import React from "react" 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 6 | import { faAsterisk } from "@fortawesome/free-solid-svg-icons" 7 | 8 | const RequiredSuperscript = () => ( 9 | 16 | 17 | 18 | ) 19 | 20 | export default RequiredSuperscript 21 | -------------------------------------------------------------------------------- /src/components/editor/property/ValuePropertyURI.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { setValuePropertyURI } from "actions/resources" 4 | import PropertyURI from "./PropertyURI" 5 | 6 | const ValuePropertyURI = ({ propertyTemplate, value, readOnly = false }) => { 7 | if (propertyTemplate.ordered) return null 8 | 9 | return ( 10 | 16 | ) 17 | } 18 | 19 | ValuePropertyURI.propTypes = { 20 | propertyTemplate: PropTypes.object.isRequired, 21 | value: PropTypes.object.isRequired, 22 | readOnly: PropTypes.bool, 23 | } 24 | 25 | export default ValuePropertyURI 26 | -------------------------------------------------------------------------------- /src/components/exports/Exports.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useMemo } from "react" 4 | import PropTypes from "prop-types" 5 | import Header from "../Header" 6 | import { useSelector } from "react-redux" 7 | import Config from "Config" 8 | import { selectExports } from "selectors/exports" 9 | import AlertsContextProvider from "components/alerts/AlertsContextProvider" 10 | import ContextAlert from "components/alerts/ContextAlert" 11 | import { exportsErrorKey } from "utilities/errorKeyFactory" 12 | 13 | const Exports = (props) => { 14 | const exportFiles = useSelector((state) => selectExports(state)) 15 | 16 | const sortedExportFiles = useMemo( 17 | () => exportFiles.sort((a, b) => a.localeCompare(b)), 18 | [exportFiles] 19 | ) 20 | 21 | const exportFileList = sortedExportFiles.map((exportFile) => ( 22 |
  • 23 | 28 | {exportFile} 29 | 30 |
  • 31 | )) 32 | 33 | return ( 34 | 35 |
    36 |
    37 | 38 |

    Exports

    39 |

    40 | Exports are regenerated weekly. Each zip file contains separate files 41 | per record (as JSON-LD). 42 |

    43 |
      {exportFileList}
    44 |
    45 |
    46 | ) 47 | } 48 | 49 | Exports.propTypes = { 50 | triggerHandleOffsetMenu: PropTypes.func, 51 | } 52 | 53 | export default Exports 54 | -------------------------------------------------------------------------------- /src/components/home/HomePage.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import Header from "./Header" 6 | import NewsPanel from "./NewsPanel" 7 | import DescPanel from "./DescPanel" 8 | 9 | const HomePage = (props) => ( 10 |
    11 |
    12 | 13 | 14 |
    15 | ) 16 | 17 | HomePage.propTypes = { 18 | triggerHandleOffsetMenu: PropTypes.func, 19 | } 20 | 21 | export default HomePage 22 | -------------------------------------------------------------------------------- /src/components/home/NewsItem.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import Package from "../../../package.json" 5 | 6 | const NewsItem = () => ( 7 |
    8 |

    Latest news

    9 |

    Sinopia Version {Package.version} highlights

    10 |

    11 | 12 | For complete release notes see the{" "} 13 | 14 | Sinopia help site 15 | 16 | . 17 | 18 |

    19 |
      20 |
    • Cached-lookups replaced with provider APIs for QA
    • 21 |
    • New autofill for Work title when creating a Work from an Instance
    • 22 |
    • 23 | New vocabularies added for relationship, note type, and serial 24 | publication type 25 |
    • 26 |
    • Updates to BF2MARC conversion
    • 27 |
    • UI/UX updates
    • 28 |
    29 |
    30 | ) 31 | 32 | export default NewsItem 33 | -------------------------------------------------------------------------------- /src/components/home/NewsPanel.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import NewsItem from "./NewsItem" 5 | import UserNotifications from "./UserNotifications" 6 | import LoginPanel from "./LoginPanel" 7 | 8 | const NewsPanel = () => ( 9 |
    10 |
    11 |
    12 | 13 |
    14 |
    15 |
    16 |
    17 | 18 |
    19 |
    20 | 21 |
    22 |
    23 |
    24 |
    25 |
    26 | ) 27 | 28 | export default NewsPanel 29 | -------------------------------------------------------------------------------- /src/components/home/UserNotifications.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import { 6 | hasUser as hasUserSelector, 7 | selectGroups, 8 | } from "selectors/authenticate" 9 | 10 | const UserNotifications = () => { 11 | const hasUser = useSelector((state) => hasUserSelector(state)) 12 | const userGroups = useSelector((state) => selectGroups(state)) 13 | 14 | if (!hasUser) return null // nothing to show if not logged in 15 | if (userGroups.length) return null // nothing to show if the user is logged in but is in at least one group 16 | 17 | if (!userGroups.length) { 18 | // show a message if the user is not in any groups 19 | return ( 20 |
    21 | Note: Before you can create new resources or edit 22 | existing resources, the Sinopia administrator will need to add you to a 23 | permission group. Please contact  24 | 25 | sinopia_admin@stanford.edu 26 | 27 |   to request edit permission. 28 |
    29 | ) 30 | } 31 | } 32 | export default UserNotifications 33 | -------------------------------------------------------------------------------- /src/components/load/LoadResource.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import Header from "../Header" 6 | import LoadByRDFForm from "./LoadByRDFForm" 7 | import AlertsContextProvider from "components/alerts/AlertsContextProvider" 8 | import ContextAlert from "components/alerts/ContextAlert" 9 | 10 | // Errors from loading a resource by RDF. 11 | const loadResourceByRDFErrorKey = "loadrdfresource" 12 | 13 | const LoadResource = (props) => ( 14 | 15 |
    16 |
    17 | 18 | 19 |
    20 |
    21 | ) 22 | 23 | LoadResource.propTypes = { 24 | triggerHandleOffsetMenu: PropTypes.func, 25 | history: PropTypes.object, 26 | } 27 | 28 | export default LoadResource 29 | -------------------------------------------------------------------------------- /src/components/menu/CanvasMenu.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState, useEffect } from "react" 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 5 | import { faTimes } from "@fortawesome/free-solid-svg-icons" 6 | import PropTypes from "prop-types" 7 | import Config from "Config" 8 | 9 | const CanvasMenu = (props) => { 10 | const [content, setContent] = useState(null) 11 | 12 | useEffect(() => { 13 | // Logging error here is OK because component displays appropriately when failed. 14 | fetch(Config.sinopiaHelpAndResourcesMenuContent) 15 | .then((response) => response.text()) 16 | .then((data) => setContent(data)) 17 | .catch((error) => 18 | console.error( 19 | `Error loading ${ 20 | Config.sinopiaHelpAndResourcesMenuContent 21 | }: ${error.toString()}` 22 | ) 23 | ) 24 | }, []) 25 | 26 | return ( 27 |
    28 | 37 | 38 | {content ? ( 39 |
    40 | ) : ( 41 |
    42 | Help and Resources not loaded. 43 |
    44 | )} 45 |
    46 | ) 47 | } 48 | 49 | CanvasMenu.propTypes = { 50 | closeHandleMenu: PropTypes.func, 51 | } 52 | 53 | export default CanvasMenu 54 | -------------------------------------------------------------------------------- /src/components/metrics/CountCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import _ from "lodash" 4 | 5 | const CountCard = ({ title, help = null, count = null, footer = null }) => ( 6 |
    7 |
    8 |
    {title}
    9 | {help &&
    {help}
    } 10 |
    11 |
    12 |
    13 | {_.isNull(count) ? ( 14 |
    15 | Loading... 16 |
    17 | ) : ( 18 | {count} 19 | )} 20 |
    21 |
    22 | {footer &&
    {footer}
    } 23 |
    24 | ) 25 | 26 | CountCard.propTypes = { 27 | title: PropTypes.string.isRequired, 28 | help: PropTypes.string, 29 | count: PropTypes.number, 30 | footer: PropTypes.oneOfType([ 31 | PropTypes.node, 32 | PropTypes.arrayOf(PropTypes.node), 33 | ]), 34 | } 35 | 36 | export default CountCard 37 | -------------------------------------------------------------------------------- /src/components/metrics/MetricsWrapper.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useEffect } from "react" 4 | import { useDispatch } from "react-redux" 5 | import PropTypes from "prop-types" 6 | import AlertsContextProvider from "components/alerts/AlertsContextProvider" 7 | import ContextAlert from "components/alerts/ContextAlert" 8 | import { metricsErrorKey } from "utilities/errorKeyFactory" 9 | import Header from "../Header" 10 | import { clearErrors } from "actions/errors" 11 | 12 | const MetricsWrapper = ({ title, children, triggerHandleOffsetMenu }) => { 13 | const dispatch = useDispatch() 14 | 15 | useEffect(() => { 16 | dispatch(clearErrors(metricsErrorKey)) 17 | }, [dispatch]) 18 | 19 | return ( 20 | 21 |
    22 |
    23 | 24 |

    {title}

    25 | {children} 26 |
    27 |
    28 | ) 29 | } 30 | MetricsWrapper.displayName = "MetricsWrapper" 31 | 32 | MetricsWrapper.propTypes = { 33 | children: PropTypes.oneOfType([ 34 | PropTypes.node, 35 | PropTypes.arrayOf(PropTypes.node), 36 | ]), 37 | triggerHandleOffsetMenu: PropTypes.func, 38 | title: PropTypes.string.isRequired, 39 | } 40 | 41 | export default MetricsWrapper 42 | -------------------------------------------------------------------------------- /src/components/metrics/ResourceCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | 7 | const ResourceCountMetric = () => { 8 | const resourceCountMetric = useMetric("getResourceCount") 9 | 10 | return ( 11 | 16 | ) 17 | } 18 | 19 | export default ResourceCountMetric 20 | -------------------------------------------------------------------------------- /src/components/metrics/ResourceCreatedCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState } from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | import GroupFilter, { defaultGroup } from "./GroupFilter" 7 | import DateRangeFilter, { 8 | defaultStartDate, 9 | defaultEndDate, 10 | } from "./DateRangeFilter" 11 | 12 | const ResourceCreatedCountMetric = () => { 13 | const [params, setParams] = useState({ 14 | startDate: defaultStartDate, 15 | endDate: defaultEndDate, 16 | group: defaultGroup, 17 | }) 18 | 19 | const resourceCreatedCountMetric = useMetric( 20 | "getResourceCreatedCount", 21 | params 22 | ) 23 | 24 | const footer = ( 25 | 26 | 27 | 28 | 29 | ) 30 | 31 | return ( 32 | 38 | ) 39 | } 40 | 41 | export default ResourceCreatedCountMetric 42 | -------------------------------------------------------------------------------- /src/components/metrics/ResourceEditedCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState } from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | import GroupFilter, { defaultGroup } from "./GroupFilter" 7 | import DateRangeFilter, { 8 | defaultStartDate, 9 | defaultEndDate, 10 | } from "./DateRangeFilter" 11 | 12 | const ResourceEditedCountMetric = () => { 13 | const [params, setParams] = useState({ 14 | startDate: defaultStartDate, 15 | endDate: defaultEndDate, 16 | group: defaultGroup, 17 | }) 18 | 19 | const resourceEditedCountMetric = useMetric("getResourceEditedCount", params) 20 | 21 | const footer = ( 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | return ( 29 | 35 | ) 36 | } 37 | 38 | export default ResourceEditedCountMetric 39 | -------------------------------------------------------------------------------- /src/components/metrics/ResourceMetrics.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import MetricsWrapper from "./MetricsWrapper" 6 | import ResourceCountMetric from "./ResourceCountMetric" 7 | import ResourceCreatedCountMetric from "./ResourceCreatedCountMetric" 8 | import ResourceEditedCountMetric from "./ResourceEditedCountMetric" 9 | 10 | const ResourceMetrics = ({ triggerHandleOffsetMenu }) => ( 11 | 15 |
    16 |
    17 | 18 |
    19 |
    20 | 21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 | ) 28 | 29 | ResourceMetrics.propTypes = { 30 | triggerHandleOffsetMenu: PropTypes.func, 31 | } 32 | 33 | export default ResourceMetrics 34 | -------------------------------------------------------------------------------- /src/components/metrics/TemplateCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | 7 | const TemplateCountMetric = () => { 8 | const templateCountMetric = useMetric("getTemplateCount") 9 | 10 | return ( 11 | 16 | ) 17 | } 18 | 19 | export default TemplateCountMetric 20 | -------------------------------------------------------------------------------- /src/components/metrics/TemplateCreatedCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState } from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | import GroupFilter, { defaultGroup } from "./GroupFilter" 7 | import DateRangeFilter, { 8 | defaultStartDate, 9 | defaultEndDate, 10 | } from "./DateRangeFilter" 11 | 12 | const TemplateCreatedCountMetric = () => { 13 | const [params, setParams] = useState({ 14 | startDate: defaultStartDate, 15 | endDate: defaultEndDate, 16 | group: defaultGroup, 17 | }) 18 | 19 | const templateCreatedCountMetric = useMetric( 20 | "getTemplateCreatedCount", 21 | params 22 | ) 23 | 24 | const footer = ( 25 | 26 | 27 | 28 | 29 | ) 30 | 31 | return ( 32 | 38 | ) 39 | } 40 | 41 | export default TemplateCreatedCountMetric 42 | -------------------------------------------------------------------------------- /src/components/metrics/TemplateEditedCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState } from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | import GroupFilter, { defaultGroup } from "./GroupFilter" 7 | import DateRangeFilter, { 8 | defaultStartDate, 9 | defaultEndDate, 10 | } from "./DateRangeFilter" 11 | 12 | const TemplateEditedCountMetric = () => { 13 | const [params, setParams] = useState({ 14 | startDate: defaultStartDate, 15 | endDate: defaultEndDate, 16 | group: defaultGroup, 17 | }) 18 | 19 | const templateEditedCountMetric = useMetric("getTemplateEditedCount", params) 20 | 21 | const footer = ( 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | return ( 29 | 35 | ) 36 | } 37 | 38 | export default TemplateEditedCountMetric 39 | -------------------------------------------------------------------------------- /src/components/metrics/TemplateFilter.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import InputTemplate from "../InputTemplate" 6 | 7 | // if you want to have have the input field pre-filled with a value (template id, label, author, etc.), put it here 8 | export const defaultTemplateId = null 9 | 10 | const TemplateFilter = ({ params, setParams }) => { 11 | const setTemplateId = (templateId) => { 12 | setParams({ 13 | ...params, 14 | templateId, 15 | }) 16 | } 17 | 18 | return ( 19 | 20 |
    21 | 24 |
    25 | 30 |
    31 |
    32 |
    33 | ) 34 | } 35 | 36 | TemplateFilter.propTypes = { 37 | params: PropTypes.object.isRequired, 38 | setParams: PropTypes.func.isRequired, 39 | } 40 | 41 | export default TemplateFilter 42 | -------------------------------------------------------------------------------- /src/components/metrics/TemplateMetrics.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import MetricsWrapper from "./MetricsWrapper" 6 | import TemplateCountMetric from "./TemplateCountMetric" 7 | import TemplateCreatedCountMetric from "./TemplateCreatedCountMetric" 8 | import TemplateEditedCountMetric from "./TemplateEditedCountMetric" 9 | import TemplateUsageCountMetric from "./TemplateUsageCountMetric" 10 | 11 | const TemplateMetrics = ({ triggerHandleOffsetMenu }) => ( 12 | 16 |
    17 |
    18 | 19 |
    20 |
    21 | 22 |
    23 |
    24 |
    25 |
    26 | 27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 | ) 34 | 35 | TemplateMetrics.propTypes = { 36 | triggerHandleOffsetMenu: PropTypes.func, 37 | } 38 | 39 | export default TemplateMetrics 40 | -------------------------------------------------------------------------------- /src/components/metrics/TemplateUsageCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState } from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | import TemplateFilter, { defaultTemplateId } from "./TemplateFilter" 7 | 8 | const TemplateUsageCountMetric = () => { 9 | const [params, setParams] = useState({ 10 | templateId: defaultTemplateId, 11 | }) 12 | 13 | const templateUsageCountMetric = useMetric( 14 | "getTemplateUsageCount", 15 | params, 16 | !!params.templateId // this prevents the metric API call from firing if there is no templateId 17 | ) 18 | 19 | const footer = ( 20 | 21 |
    22 | 25 |
    {params.templateId}
    26 |
    27 | 28 |
    29 | ) 30 | 31 | return ( 32 | 38 | ) 39 | } 40 | 41 | export default TemplateUsageCountMetric 42 | -------------------------------------------------------------------------------- /src/components/metrics/UserCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | 7 | const UserCountMetric = () => { 8 | const userCountMetric = useMetric("getUserCount") 9 | 10 | return ( 11 | 16 | ) 17 | } 18 | 19 | export default UserCountMetric 20 | -------------------------------------------------------------------------------- /src/components/metrics/UserMetrics.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import MetricsWrapper from "./MetricsWrapper" 6 | import UserCountMetric from "./UserCountMetric" 7 | import UserResourceCountMetric from "./UserResourceCountMetric" 8 | import UserTemplateCountMetric from "./UserTemplateCountMetric" 9 | 10 | const UserMetrics = ({ triggerHandleOffsetMenu }) => ( 11 | 15 |
    16 |
    17 | 18 |
    19 |
    20 | 21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 | ) 28 | 29 | UserMetrics.propTypes = { 30 | triggerHandleOffsetMenu: PropTypes.func, 31 | } 32 | 33 | export default UserMetrics 34 | -------------------------------------------------------------------------------- /src/components/metrics/UserResourceCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState } from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | import DateRangeFilter, { 7 | defaultStartDate, 8 | defaultEndDate, 9 | } from "./DateRangeFilter" 10 | 11 | const UserResourceCountMetric = () => { 12 | const [params, setParams] = useState({ 13 | startDate: defaultStartDate, 14 | endDate: defaultEndDate, 15 | }) 16 | 17 | const userResourceCountMetric = useMetric("getResourceUserCount", params) 18 | 19 | const footer = ( 20 | 21 | 22 | 23 | ) 24 | 25 | return ( 26 | 32 | ) 33 | } 34 | 35 | export default UserResourceCountMetric 36 | -------------------------------------------------------------------------------- /src/components/metrics/UserTemplateCountMetric.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React, { useState } from "react" 4 | import CountCard from "./CountCard" 5 | import useMetric from "hooks/useMetric" 6 | import DateRangeFilter, { 7 | defaultStartDate, 8 | defaultEndDate, 9 | } from "./DateRangeFilter" 10 | 11 | const UserTemplateCountMetric = () => { 12 | const [params, setParams] = useState({ 13 | startDate: defaultStartDate, 14 | endDate: defaultEndDate, 15 | }) 16 | 17 | const userTemplateCountMetric = useMetric("getTemplateUserCount", params) 18 | 19 | const footer = ( 20 | 21 | 22 | 23 | ) 24 | 25 | return ( 26 | 32 | ) 33 | } 34 | 35 | export default UserTemplateCountMetric 36 | -------------------------------------------------------------------------------- /src/components/search/GroupFilter.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | import React from "react" 3 | import { useSelector } from "react-redux" 4 | import { selectGroupMap } from "selectors/groups" 5 | import SearchFilter from "./SearchFilter" 6 | 7 | const GroupFilter = () => { 8 | const groupMap = useSelector((state) => selectGroupMap(state)) 9 | 10 | const filterLabelFunc = (key) => groupMap[key] || "Unknown" 11 | 12 | return ( 13 | 19 | ) 20 | } 21 | 22 | export default GroupFilter 23 | -------------------------------------------------------------------------------- /src/components/search/SearchResultRows.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import PropTypes from "prop-types" 6 | import usePermissions from "hooks/usePermissions" 7 | import { selectGroupMap } from "selectors/groups" 8 | import SearchResultRow from "./SearchResultRow" 9 | 10 | /** 11 | * Generates HTML rows of all search results 12 | */ 13 | const SearchResultRows = ({ searchResults }) => { 14 | const { canEdit, canCreate } = usePermissions() 15 | const groupMap = useSelector((state) => selectGroupMap(state)) 16 | 17 | return searchResults.map((row) => ( 18 | 25 | )) 26 | } 27 | 28 | SearchResultRows.propTypes = { 29 | searchResults: PropTypes.array.isRequired, 30 | } 31 | 32 | export default SearchResultRows 33 | -------------------------------------------------------------------------------- /src/components/search/TemplateGuessSearchResults.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | import React from "react" 3 | import { useSelector } from "react-redux" 4 | import { selectSearchResults } from "selectors/search" 5 | import ExpandingResourceTemplates from "../templates/ExpandingResourceTemplates" 6 | 7 | const TemplateGuessSearchResults = () => { 8 | const searchResults = useSelector((state) => 9 | selectSearchResults(state, "templateguess") 10 | ) 11 | 12 | return ( 13 | 18 | ) 19 | } 20 | 21 | export default TemplateGuessSearchResults 22 | -------------------------------------------------------------------------------- /src/components/search/TypeFilter.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | import React from "react" 3 | import SearchFilter from "./SearchFilter" 4 | 5 | const TypeFilter = () => ( 6 | 11 | ) 12 | 13 | export default TypeFilter 14 | -------------------------------------------------------------------------------- /src/components/templates/ExpandingResourceTemplates.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import ResourceTemplateSearchResult from "./ResourceTemplateSearchResult" 6 | import _ from "lodash" 7 | 8 | const ExpandingResourceTemplates = ({ results, id, label }) => { 9 | if (_.isEmpty(results)) return null 10 | 11 | return ( 12 |
    13 |
    14 |

    15 | 22 |

    23 |
    24 |
    25 | 26 |
    27 |
    28 | ) 29 | } 30 | 31 | ExpandingResourceTemplates.propTypes = { 32 | results: PropTypes.array, 33 | label: PropTypes.string.isRequired, 34 | id: PropTypes.string.isRequired, 35 | } 36 | 37 | export default ExpandingResourceTemplates 38 | -------------------------------------------------------------------------------- /src/components/templates/ResourceTemplate.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import Header from "../Header" 6 | import TemplateSearch from "./TemplateSearch" 7 | import AlertsContextProvider from "components/alerts/AlertsContextProvider" 8 | import ContextAlert from "components/alerts/ContextAlert" 9 | import { templateErrorKey } from "utilities/errorKeyFactory" 10 | import PreviewModal from "../editor/preview/PreviewModal" 11 | 12 | const ResourceTemplate = (props) => ( 13 | 14 |
    15 |
    16 | 17 | 18 | 19 |
    20 |
    21 | ) 22 | 23 | ResourceTemplate.propTypes = { 24 | children: PropTypes.array, 25 | triggerHandleOffsetMenu: PropTypes.func, 26 | history: PropTypes.object, 27 | } 28 | 29 | export default ResourceTemplate 30 | -------------------------------------------------------------------------------- /src/components/templates/ResourceTemplateSearchResult.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import PropTypes from "prop-types" 5 | import ResourceTemplateRow from "./ResourceTemplateRow" 6 | 7 | /** 8 | * This is the list view of all the templates 9 | */ 10 | const ResourceTemplateSearchResult = ({ results }) => { 11 | const rows = results.map((row) => ( 12 | 13 | )) 14 | 15 | return ( 16 |
    17 |
    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | {rows} 33 |
    Label / IDResource URIAuthorGroupDateGuiding statement 28 | Actions 29 |
    34 |
    35 |
    36 | ) 37 | } 38 | 39 | ResourceTemplateSearchResult.propTypes = { 40 | results: PropTypes.array, 41 | } 42 | 43 | export default ResourceTemplateSearchResult 44 | -------------------------------------------------------------------------------- /src/components/templates/SinopiaResourceTemplates.jsx: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import { useSelector } from "react-redux" 5 | import ResourceTemplateSearchResult from "./ResourceTemplateSearchResult" 6 | import { selectHistoricalTemplates } from "selectors/history" 7 | import { selectSearchResults } from "selectors/search" 8 | import ExpandingResourceTemplates from "./ExpandingResourceTemplates" 9 | import _ from "lodash" 10 | 11 | /** 12 | * This is the list view of all the templates 13 | */ 14 | const SinopiaResourceTemplates = () => { 15 | const searchResults = useSelector((state) => 16 | selectSearchResults(state, "template") 17 | ) 18 | const historicalTemplates = useSelector((state) => 19 | selectHistoricalTemplates(state) 20 | ) 21 | 22 | return ( 23 |
    24 | 29 | {_.isEmpty(searchResults) ? ( 30 |
    31 | No resource templates match. 32 |
    33 | ) : ( 34 | 35 | )} 36 |
    37 | ) 38 | } 39 | 40 | export default SinopiaResourceTemplates 41 | -------------------------------------------------------------------------------- /src/hooks/useAlerts.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { AlertsContext } from "components/alerts/AlertsContextProvider" 3 | 4 | const useAlerts = () => { 5 | const context = React.useContext(AlertsContext) 6 | if (!context) { 7 | throw new Error("useAlerts must be used within an AlertsProvider") 8 | } 9 | return context 10 | } 11 | 12 | export default useAlerts 13 | -------------------------------------------------------------------------------- /src/hooks/useEditor.js: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux" 2 | import { clearResource } from "actions/resources" 3 | import { selectResourceKeys } from "selectors/resources" 4 | import { useHistory } from "react-router-dom" 5 | 6 | const useEditor = (resourceKey) => { 7 | const dispatch = useDispatch() 8 | const history = useHistory() 9 | 10 | const resourceKeyCount = useSelector( 11 | (state) => selectResourceKeys(state).length 12 | ) 13 | 14 | const handleCloseResource = (event) => { 15 | if (event) event.preventDefault() 16 | 17 | dispatch(clearResource(resourceKey)) 18 | // If this is the last resource, then return to dashboard. 19 | if (resourceKeyCount <= 1) history.push("/dashboard") 20 | } 21 | 22 | return { handleCloseResource } 23 | } 24 | 25 | export default useEditor 26 | -------------------------------------------------------------------------------- /src/hooks/useLeftNav.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux" 2 | import { 3 | showNavProperty, 4 | hideNavProperty, 5 | showNavSubject, 6 | hideNavSubject, 7 | } from "actions/resources" 8 | 9 | const useLeftNav = (navObj) => { 10 | // navObj can be a subject or property. 11 | const dispatch = useDispatch() 12 | const isExpanded = navObj.showNav 13 | 14 | const handleToggleClick = (event) => { 15 | event.preventDefault() 16 | 17 | if (navObj.subjectTemplateKey) { 18 | if (isExpanded) { 19 | dispatch(hideNavSubject(navObj.key)) 20 | } else { 21 | dispatch(showNavSubject(navObj.key)) 22 | } 23 | } else if (isExpanded) { 24 | dispatch(hideNavProperty(navObj.key)) 25 | } else { 26 | dispatch(showNavProperty(navObj.key)) 27 | } 28 | } 29 | 30 | return { handleToggleClick, isExpanded } 31 | } 32 | 33 | export default useLeftNav 34 | -------------------------------------------------------------------------------- /src/hooks/useMetric.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from "react" 2 | import { useDispatch } from "react-redux" 3 | import { metricsErrorKey } from "utilities/errorKeyFactory" 4 | import { addError } from "actions/errors" 5 | import * as sinopiaMetrics from "../sinopiaMetrics" 6 | 7 | const useMetric = (name, params = null, runMetric = true) => { 8 | const dispatch = useDispatch() 9 | const [metric, setMetric] = useState(null) 10 | const isMountedRef = useRef(false) 11 | 12 | useEffect(() => { 13 | isMountedRef.current = true 14 | return () => { 15 | isMountedRef.current = false 16 | } 17 | }, []) 18 | 19 | useEffect(() => { 20 | if (!runMetric) return setMetric({ count: 0 }) 21 | sinopiaMetrics[name](params || {}) 22 | .then((results) => { 23 | if (isMountedRef.current) setMetric(results) 24 | }) 25 | .catch((err) => { 26 | if (isMountedRef.current) { 27 | dispatch( 28 | addError( 29 | metricsErrorKey, 30 | `Error retrieving metrics: ${err.message || err}` 31 | ) 32 | ) 33 | } 34 | }) 35 | }, [name, params, dispatch, runMetric]) 36 | 37 | return metric 38 | } 39 | 40 | export default useMetric 41 | -------------------------------------------------------------------------------- /src/hooks/useNavLink.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import { setCurrentComponent } from "actions/index" 3 | import { useDispatch, useSelector } from "react-redux" 4 | import { 5 | isCurrentProperty as isCurrentPropertySelector, 6 | isCurrentComponent as isCurrentComponentSelector, 7 | } from "selectors/index" 8 | import { stickyScrollIntoView } from "utilities/Utilities" 9 | 10 | const useNavLink = (navObj) => { 11 | const dispatch = useDispatch() 12 | 13 | const isCurrentProperty = useSelector((state) => 14 | isCurrentPropertySelector( 15 | state, 16 | navObj.rootSubjectKey, 17 | navObj.rootPropertyKey 18 | ) 19 | ) 20 | const isCurrentComponent = useSelector((state) => 21 | isCurrentComponentSelector(state, navObj.rootSubjectKey, navObj.key) 22 | ) 23 | const navLinkId = `navLink-${navObj.key}` 24 | const navTargetId = `navTarget-${navObj.key}` 25 | 26 | // This causes the component to scroll into view when first mounted if current component. 27 | useEffect(() => { 28 | if (!isCurrentComponent) return 29 | 30 | stickyScrollIntoView(`#${navLinkId}`) 31 | 32 | // This is only on initial mount 33 | // eslint-disable-next-line react-hooks/exhaustive-deps 34 | }, []) 35 | 36 | const handleNavLinkClick = (event) => { 37 | event.preventDefault() 38 | 39 | stickyScrollIntoView(`#${navTargetId}`) 40 | dispatch( 41 | setCurrentComponent( 42 | navObj.rootSubjectKey, 43 | navObj.rootPropertyKey, 44 | navObj.key 45 | ) 46 | ) 47 | } 48 | 49 | return { navLinkId, handleNavLinkClick, isCurrentProperty } 50 | } 51 | 52 | export default useNavLink 53 | -------------------------------------------------------------------------------- /src/hooks/useNavTarget.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import { setCurrentComponent } from "actions/index" 3 | import { useDispatch, useSelector } from "react-redux" 4 | import { isCurrentComponent as isCurrentComponentSelector } from "selectors/index" 5 | import { stickyScrollIntoView } from "utilities/Utilities" 6 | 7 | const useNavTarget = (navObj) => { 8 | const dispatch = useDispatch() 9 | 10 | const isCurrentComponent = useSelector((state) => 11 | isCurrentComponentSelector(state, navObj.rootSubjectKey, navObj.key) 12 | ) 13 | 14 | const navTargetId = `navTarget-${navObj.key}` 15 | 16 | // This causes the component to scroll into view when first mounted if current component. 17 | useEffect(() => { 18 | if (!isCurrentComponent) return 19 | 20 | window.scrollTo(0, 0) 21 | stickyScrollIntoView(`#${navTargetId}`) 22 | 23 | // This is only on initial mount 24 | // eslint-disable-next-line react-hooks/exhaustive-deps 25 | }, []) 26 | 27 | const handleNavTargetClick = (event) => { 28 | dispatch( 29 | setCurrentComponent( 30 | navObj.rootSubjectKey, 31 | navObj.rootPropertyKey, 32 | navObj.key 33 | ) 34 | ) 35 | event.stopPropagation() 36 | } 37 | 38 | return { navTargetId, handleNavTargetClick } 39 | } 40 | 41 | export default useNavTarget 42 | -------------------------------------------------------------------------------- /src/hooks/usePermissions.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux" 2 | import { selectGroups } from "selectors/authenticate" 3 | import _ from "lodash" 4 | 5 | const usePermissions = () => { 6 | const userGroups = useSelector((state) => selectGroups(state)) || [] 7 | 8 | const canEdit = (resource) => 9 | userGroups.includes(resource?.group) || 10 | !!_.intersection(userGroups, resource?.editGroups).length 11 | 12 | const canChangeGroups = (resource) => userGroups.includes(resource.group) 13 | 14 | return { canCreate: !!userGroups.length, canEdit, canChangeGroups } 15 | } 16 | 17 | export default usePermissions 18 | -------------------------------------------------------------------------------- /src/hooks/useResourcHasChanged.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react" 2 | import { useDispatch } from "react-redux" 3 | import { setResourceChanged } from "actions/resources" 4 | 5 | const useResourceHasChanged = (value) => { 6 | const dispatch = useDispatch() 7 | 8 | // This indicates whether on a SET_RESOURCE_CHANGED has been dispatched. 9 | // Using a ref for this because don't want to trigger rerender when changes. 10 | const hasDispatchedChanged = useRef(false) 11 | 12 | useEffect(() => { 13 | hasDispatchedChanged.current = false 14 | }, [value]) 15 | 16 | const handleKeyDown = () => { 17 | if (!hasDispatchedChanged.current) { 18 | dispatch(setResourceChanged(value.rootSubjectKey)) 19 | hasDispatchedChanged.current = true 20 | } 21 | } 22 | 23 | return handleKeyDown 24 | } 25 | 26 | export default useResourceHasChanged 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import React from "react" 4 | import ReactDOM from "react-dom" 5 | import RootContainer from "./components/RootContainer" 6 | import "@popperjs/core" 7 | import "bootstrap" 8 | 9 | const root = document.createElement("div") 10 | root.className = "container-fluid" 11 | document.body.appendChild(root) 12 | 13 | ReactDOM.render(React.createElement(RootContainer), root) 14 | -------------------------------------------------------------------------------- /src/reducers/authenticate.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Stanford University see LICENSE for license 2 | 3 | export const setUser = (state, action) => { 4 | const newState = { ...state } 5 | newState.user = { ...action.payload } 6 | return newState 7 | } 8 | 9 | export const removeUser = (state) => { 10 | const newState = { ...state } 11 | delete newState.user 12 | return newState 13 | } 14 | -------------------------------------------------------------------------------- /src/reducers/errors.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Stanford University see LICENSE for license 2 | 3 | import { hideModal } from "./modals" 4 | 5 | /** 6 | * Hide validation errors 7 | * @param {Object} state the previous redux state 8 | * @return {Object} the next redux state 9 | */ 10 | export const hideValidationErrors = (state, action) => 11 | setValidationError(state, action.payload, false) 12 | 13 | export const addError = (state, action) => ({ 14 | ...state, 15 | errors: { 16 | ...state.errors, 17 | [action.payload.errorKey]: [ 18 | ...(state.errors[action.payload.errorKey] || []), 19 | action.payload.error, 20 | ], 21 | }, 22 | }) 23 | 24 | export const clearErrors = (state, action) => ({ 25 | ...state, 26 | errors: { 27 | ...state.errors, 28 | [action.payload]: [], 29 | }, 30 | }) 31 | 32 | /** 33 | * Close modals and show validation errors 34 | * @param {Object} state the previous redux state 35 | * @return {Object} the next redux state 36 | */ 37 | export const showValidationErrors = (state, action) => { 38 | const newState = hideModal(state) 39 | return setValidationError(newState, action.payload, true) 40 | } 41 | 42 | const setValidationError = (state, resourceKey, value) => ({ 43 | ...state, 44 | resourceValidation: { 45 | ...state.resourceValidation, 46 | [resourceKey]: value, 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /src/reducers/exports.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | export const exportsReceived = (state, action) => ({ 4 | ...state, 5 | exports: action.payload, 6 | }) 7 | 8 | export const noop = () => {} 9 | -------------------------------------------------------------------------------- /src/reducers/groups.js: -------------------------------------------------------------------------------- 1 | export const groupsReceived = (state, action) => ({ 2 | ...state, 3 | groups: action.payload, 4 | groupMap: createGroupMap(action.payload), 5 | }) 6 | 7 | export const createGroupMap = (groupList) => { 8 | const groupMap = {} 9 | groupList.forEach((group) => (groupMap[group.id] = group.label)) 10 | return groupMap 11 | } 12 | 13 | export const noop = () => {} 14 | -------------------------------------------------------------------------------- /src/reducers/languages.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Stanford University see LICENSE for license 2 | import { setSubjectChanged } from "reducers/resources" 3 | import { 4 | mergeSubjectPropsToNewState, 5 | recursiveDescFromSubject, 6 | } from "./resourceHelpers" 7 | import _ from "lodash" 8 | 9 | export const setLanguage = (state, action) => { 10 | const valueKey = action.payload.valueKey 11 | const value = state.values[valueKey] 12 | const property = state.properties[value.propertyKey] 13 | const newState = setSubjectChanged(state, property.subjectKey, true) 14 | return { 15 | ...newState, 16 | values: { 17 | ...newState.values, 18 | [valueKey]: { 19 | ...newState.values[valueKey], 20 | lang: action.payload.lang, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | export const languagesReceived = (state, action) => ({ 27 | ...state, 28 | ...action.payload, 29 | }) 30 | 31 | export const setDefaultLang = (state, action) => { 32 | const { resourceKey, lang } = action.payload 33 | const newState = mergeSubjectPropsToNewState(state, resourceKey, { 34 | defaultLang: lang, 35 | }) 36 | 37 | const updateLang = (value) => { 38 | if (value.component === "InputLiteralValue" && _.isEmpty(value.literal)) { 39 | value.lang = lang 40 | } 41 | if (value.component === "InputURIValue" && _.isEmpty(value.label)) { 42 | value.lang = lang 43 | } 44 | } 45 | 46 | return recursiveDescFromSubject(newState, resourceKey, updateLang) 47 | } 48 | -------------------------------------------------------------------------------- /src/reducers/lookups.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Stanford University see LICENSE for license 2 | 3 | /** 4 | * Adds a lookup to state. 5 | * @param {Object} state the previous redux state 6 | * @param {Object} action to be performed 7 | * @return {Object} the next redux state 8 | */ 9 | export const lookupOptionsRetrieved = (state, action) => { 10 | const lookups = [...action.payload.lookup] 11 | lookups.sort((item1, item2) => { 12 | if (item1.label < item2.label) { 13 | return -1 14 | } 15 | if (item1.label > item2.label) { 16 | return 1 17 | } 18 | return 0 19 | }) 20 | return { 21 | ...state, 22 | lookups: { 23 | ...state.lookups, 24 | [action.payload.uri]: lookups, 25 | }, 26 | } 27 | } 28 | 29 | export const noop = () => {} 30 | -------------------------------------------------------------------------------- /src/reducers/messages.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | /** 4 | * @param {Object} state the previous redux state 5 | * @param {Object} action the payload of the action is a boolean that says to show or not to show the CopyNewMessage 6 | * @return {Object} the next redux state 7 | */ 8 | export const showCopyNewMessage = (state, action) => ({ 9 | ...state, 10 | copyToNewMessage: { 11 | timestamp: action.payload.timestamp, 12 | oldUri: action.payload.oldUri, 13 | }, 14 | }) 15 | 16 | export const noop = () => {} 17 | -------------------------------------------------------------------------------- /src/reducers/modals.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash" 2 | 3 | export const showModal = (state, action) => setModal(state, action.payload) 4 | 5 | export const hideModal = (state) => setModal(state, null) 6 | 7 | export const showLangModal = (state, action) => 8 | setModal(state, "LangModal", { currentLangModalValue: action.payload }) 9 | 10 | export const showMarcModal = (state, action) => 11 | setModal(state, "MarcModal", { marc: action.payload }) 12 | 13 | const setModal = ( 14 | state, 15 | name, 16 | { currentLangModalValue = null, marc = null } = {} 17 | ) => { 18 | let newCurrentModal 19 | if (name) { 20 | newCurrentModal = [...state.currentModal, name] 21 | } else { 22 | newCurrentModal = _.dropRight(state.currentModal) 23 | } 24 | return { 25 | ...state, 26 | currentModal: newCurrentModal, 27 | currentLangModalValue, 28 | marc, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/reducers/relationships.js: -------------------------------------------------------------------------------- 1 | export const setRelationships = (state, action) => ({ 2 | ...state, 3 | relationships: { 4 | ...state.relationships, 5 | [action.payload.resourceKey]: action.payload.relationships, 6 | }, 7 | }) 8 | 9 | export const clearRelationships = (state, action) => { 10 | const newRelationships = { ...state.relationships } 11 | delete newRelationships[action.payload] 12 | 13 | return { 14 | ...state, 15 | relationships: newRelationships, 16 | } 17 | } 18 | 19 | export const setSearchRelationships = (state, action) => { 20 | if (!state.resource) return state 21 | 22 | return { 23 | ...state, 24 | resource: { 25 | ...state.resource, 26 | relationshipResults: { 27 | ...state.resource.relationshipResults, 28 | [action.payload.uri]: action.payload.relationships, 29 | }, 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/reducers/search.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | import { defaultSearchResultsPerPage } from "utilities/Search" 3 | 4 | /** 5 | * Sets state for search results. 6 | * @param {Object} state the previous redux state 7 | * @param {Object} action the payload of the action is the this of search results 8 | * @return {Object} the next redux state 9 | */ 10 | export const setSearchResults = (state, action) => ({ 11 | ...state, 12 | [action.payload.searchType]: { 13 | uri: action.payload.uri, 14 | results: action.payload.results, 15 | totalResults: action.payload.totalResults, 16 | facetResults: action.payload.facetResults || {}, 17 | relationshipResults: {}, 18 | query: action.payload.query, 19 | options: { 20 | resultsPerPage: 21 | action.payload.options?.resultsPerPage || 22 | defaultSearchResultsPerPage(action.payload.searchType), 23 | startOfRange: action.payload.options?.startOfRange || 0, 24 | sortField: action.payload.options?.sortField, 25 | sortOrder: action.payload.options?.sortOrder, 26 | typeFilter: action.payload.options?.typeFilter, 27 | groupFilter: action.payload.options?.groupFilter, 28 | }, 29 | error: action.payload.error, 30 | }, 31 | }) 32 | 33 | /** 34 | * Clears existing state related to search results. 35 | * @param {Object} state the previous redux state 36 | * @return {Object} the next redux state 37 | */ 38 | export const clearSearchResults = (state, action) => ({ 39 | ...state, 40 | [action.payload]: null, 41 | }) 42 | 43 | export const setHeaderSearch = (state, action) => ({ 44 | ...state, 45 | currentHeaderSearch: action.payload, 46 | }) 47 | -------------------------------------------------------------------------------- /src/reducers/templates.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | export const addTemplates = (state, action) => { 4 | const newSubjectTemplate = { ...action.payload } 5 | 6 | const newState = { 7 | ...state, 8 | subjectTemplates: { ...state.subjectTemplates }, 9 | propertyTemplates: { ...state.propertyTemplates }, 10 | } 11 | 12 | newSubjectTemplate.propertyTemplates.forEach((propertyTemplate) => { 13 | newState.propertyTemplates[propertyTemplate.key] = { ...propertyTemplate } 14 | }) 15 | delete newSubjectTemplate.propertyTemplates 16 | newState.subjectTemplates[newSubjectTemplate.key] = newSubjectTemplate 17 | 18 | return newState 19 | } 20 | 21 | export const noop = () => {} 22 | -------------------------------------------------------------------------------- /src/selectors/authenticate.js: -------------------------------------------------------------------------------- 1 | export const hasUser = (state) => !!state.authenticate.user 2 | 3 | export const selectUser = (state) => state.authenticate.user 4 | 5 | export const selectGroups = (state) => state.authenticate.user?.groups 6 | -------------------------------------------------------------------------------- /src/selectors/errors.js: -------------------------------------------------------------------------------- 1 | import { selectProperty, selectSubject, selectNormSubject } from "./resources" 2 | import _ from "lodash" 3 | 4 | /** 5 | * Determines if resource validation should be displayed. 6 | * @param {Object} state the redux state 7 | * @param {string} resourceKey of the resource to check; if omitted, current resource key is used 8 | * @return {boolean} true if resource validations should be displayed 9 | */ 10 | export const displayResourceValidations = (state, resourceKey) => 11 | !!state.editor.resourceValidation[resourceKey] 12 | 13 | export const hasValidationErrors = (state, resourceKey) => { 14 | const subject = selectNormSubject(state, resourceKey) 15 | return !_.isEmpty(subject?.descWithErrorPropertyKeys) 16 | } 17 | 18 | /** 19 | * @returns {function} a function that returns the errors for an error key 20 | */ 21 | export const selectErrors = (state, errorKey) => state.editor.errors[errorKey] 22 | 23 | export const selectValidationErrors = (state, resourceKey) => { 24 | const subject = selectSubject(state, resourceKey) 25 | if (subject == null) return [] 26 | 27 | const errors = [] 28 | 29 | subject.descWithErrorPropertyKeys.forEach((propertyKey) => { 30 | const property = selectProperty(state, propertyKey) 31 | if ( 32 | property.descWithErrorPropertyKeys.length === 1 && 33 | property.values !== null 34 | ) { 35 | property.values.forEach((value) => { 36 | value.errors.forEach((error) => { 37 | const newError = { 38 | message: error, 39 | propertyKey: property.key, 40 | labelPath: property.labels, 41 | } 42 | errors.push(newError) 43 | }) 44 | }) 45 | } 46 | }) 47 | return errors 48 | } 49 | -------------------------------------------------------------------------------- /src/selectors/exports.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | export const selectExports = (state) => state.entities.exports 4 | 5 | export const hasExports = (state) => state.entities.exports.length > 0 6 | -------------------------------------------------------------------------------- /src/selectors/groups.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Stanford University see LICENSE for license 2 | import _ from "lodash" 3 | 4 | export const hasGroups = (state) => !_.isEmpty(state.entities.groupMap) 5 | 6 | export const selectGroupMap = (state) => state.entities.groupMap 7 | 8 | export const noop = () => {} 9 | -------------------------------------------------------------------------------- /src/selectors/history.js: -------------------------------------------------------------------------------- 1 | export const selectHistoricalTemplates = (state) => state.history.templates 2 | 3 | export const selectHistoricalSearches = (state) => state.history.searches 4 | 5 | export const selectHistoricalResources = (state) => state.history.resources 6 | -------------------------------------------------------------------------------- /src/selectors/index.js: -------------------------------------------------------------------------------- 1 | export const selectCurrentComponentKey = (state, resourceKey) => 2 | state.editor.currentComponent[resourceKey]?.component 3 | 4 | export const selectCurrentPropertyKey = (state, resourceKey) => 5 | state.editor.currentComponent[resourceKey]?.property 6 | 7 | export const isCurrentComponent = (state, resourceKey, componentKey) => 8 | state.editor.currentComponent[resourceKey]?.component === componentKey 9 | 10 | export const isCurrentProperty = (state, resourceKey, propertyKey) => 11 | state.editor.currentComponent[resourceKey]?.property === propertyKey 12 | 13 | export const noop = () => {} 14 | -------------------------------------------------------------------------------- /src/selectors/languages.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | import { parseLangTag } from "utilities/Language" 3 | import { selectNormSubject } from "./resources" 4 | 5 | export const selectLanguageLabel = (state, tag) => { 6 | if (!tag) return "No language specified" 7 | // Cheat. 8 | if (tag === "en") return "English" 9 | 10 | const [langSubtag, scriptSubtag, transliterationSubtag] = parseLangTag(tag) 11 | const labels = [ 12 | state.entities.languages[langSubtag] || `Unknown language (${langSubtag})`, 13 | ] 14 | if (scriptSubtag) 15 | labels.push( 16 | state.entities.scripts[scriptSubtag] || `Unknown script (${scriptSubtag})` 17 | ) 18 | if (transliterationSubtag) 19 | labels.push( 20 | state.entities.transliterations[transliterationSubtag] || 21 | `Unknown transliteration (${transliterationSubtag})` 22 | ) 23 | return labels.join(" - ") 24 | } 25 | 26 | export const hasLanguages = (state) => { 27 | state.entities.languages.length > 0 28 | } 29 | 30 | export const selectLanguages = (state) => state.entities.languageLookup 31 | 32 | export const selectLanguageLabels = (state) => state.entities.languages 33 | 34 | export const selectScripts = (state) => state.entities.scriptLookup 35 | 36 | export const selectTransliterations = (state) => 37 | state.entities.transliterationLookup 38 | 39 | export const selectDefaultLang = (state, resourceKey) => 40 | selectNormSubject(state, resourceKey)?.defaultLang 41 | -------------------------------------------------------------------------------- /src/selectors/lookups.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | /** 4 | * Return lookup based on URI. 5 | * @param [Object] state 6 | * @param [string] URI of the lookup 7 | * @return [Object] the lookup if found 8 | */ 9 | export const selectLookup = (state, uri) => state.entities.lookups[uri] 10 | 11 | export const noop = () => {} 12 | -------------------------------------------------------------------------------- /src/selectors/messages.js: -------------------------------------------------------------------------------- 1 | export const selectCopyToNewMessageOldUri = (state) => 2 | state.editor.copyToNewMessage.oldUri 3 | 4 | export const selectCopyToNewMessageTimestamp = (state) => 5 | state.editor.copyToNewMessage.timestamp 6 | -------------------------------------------------------------------------------- /src/selectors/modals.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | import _ from "lodash" 3 | 4 | export const selectCurrentModal = (state) => 5 | _.last(state.editor.currentModal) || null 6 | 7 | export const selectUnusedRDF = (state, resourceKey) => 8 | state.editor.unusedRDF[resourceKey] 9 | 10 | export const isModalOpen = (state) => !_.isEmpty(state.editor.currentModal) 11 | 12 | export const isCurrentModal = (state, name) => 13 | selectCurrentModal(state) === name 14 | 15 | export const selectCurrentLangModalValue = (state) => 16 | state.editor.currentLangModalValue 17 | 18 | export const selectMarc = (state) => state.editor.marc 19 | -------------------------------------------------------------------------------- /src/selectors/relationships.js: -------------------------------------------------------------------------------- 1 | import { selectNormSubject } from "./resources" 2 | import _ from "lodash" 3 | 4 | // Merges relationships from the resource and inferred relationships 5 | export const selectRelationships = (state, resourceKey) => { 6 | const resource = selectNormSubject(state, resourceKey) || emptyRelationships 7 | const relationships = 8 | state.entities.relationships[resourceKey] || emptyRelationships 9 | 10 | const mergeRelationship = (field) => { 11 | const resourceRelationships = resource[field] || [] 12 | const inferredRelationships = relationships[field] || [] 13 | return _.uniq([...resourceRelationships, ...inferredRelationships]) 14 | } 15 | 16 | return { 17 | bfAdminMetadataRefs: mergeRelationship("bfAdminMetadataRefs"), 18 | bfItemRefs: mergeRelationship("bfItemRefs"), 19 | bfInstanceRefs: mergeRelationship("bfInstanceRefs"), 20 | bfWorkRefs: mergeRelationship("bfWorkRefs"), 21 | } 22 | } 23 | 24 | const emptyRelationships = { 25 | bfAdminMetadataRefs: [], 26 | bfItemRefs: [], 27 | bfInstanceRefs: [], 28 | bfWorkRefs: [], 29 | } 30 | 31 | export const hasRelationships = (state, resourceKey) => 32 | !isEmpty(selectRelationships(state, resourceKey)) 33 | 34 | export const hasSearchRelationships = (state, uri) => 35 | !isEmpty(selectSearchRelationships(state, uri)) 36 | 37 | export const selectSearchRelationships = (state, uri) => { 38 | const relationshipResults = state.search.resource?.relationshipResults || {} 39 | return relationshipResults[uri] 40 | } 41 | 42 | const isEmpty = (relationships) => { 43 | if (_.isEmpty(relationships)) return true 44 | return Object.values(relationships).every((refs) => _.isEmpty(refs)) 45 | } 46 | -------------------------------------------------------------------------------- /src/selectors/search.js: -------------------------------------------------------------------------------- 1 | import { defaultSearchResultsPerPage } from "utilities/Search" 2 | 3 | export const selectSearchError = (state, searchType) => 4 | state.search[searchType]?.error 5 | 6 | export const selectSearchUri = (state, searchType) => 7 | state.search[searchType]?.uri 8 | 9 | export const selectSearchQuery = (state, searchType) => 10 | state.search[searchType]?.query 11 | 12 | export const selectSearchTotalResults = (state, searchType) => 13 | state.search[searchType]?.totalResults || 0 14 | 15 | export const selectSearchFacetResults = (state, searchType, facetType) => 16 | state.search[searchType]?.facetResults[facetType] 17 | 18 | export const selectSearchOptions = (state, searchType) => 19 | state.search[searchType]?.options || { 20 | startOfRange: 0, 21 | resultsPerPage: defaultSearchResultsPerPage(searchType), 22 | } 23 | 24 | export const selectSearchResults = (state, searchType) => 25 | state.search[searchType]?.results 26 | 27 | export const selectHeaderSearch = (state) => state.editor.currentHeaderSearch 28 | -------------------------------------------------------------------------------- /src/selectors/templates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Selects a subject template by key. 3 | * @param [Object] state 4 | * @param [string] key 5 | * @return [Object] subject template 6 | */ 7 | export const selectSubjectTemplate = (state, key) => 8 | state.entities.subjectTemplates[key] 9 | 10 | /** 11 | * Selects a property template by key. 12 | * @param [Object] state 13 | * @param [string] key 14 | * @return [Object] property template 15 | */ 16 | export const selectPropertyTemplate = (state, key) => 17 | state.entities.propertyTemplates[key] 18 | 19 | /** 20 | * Selects a subject template and associated property templates by key. 21 | * @param [Object] state 22 | * @param [string] key 23 | * @return [Object] subject template 24 | */ 25 | export const selectSubjectAndPropertyTemplates = (state, key) => { 26 | const subjectTemplate = selectSubjectTemplate(state, key) 27 | if (!subjectTemplate) return null 28 | 29 | const newSubjectTemplate = { ...subjectTemplate } 30 | newSubjectTemplate.propertyTemplates = 31 | subjectTemplate.propertyTemplateKeys.map((propertyTemplateKey) => 32 | selectPropertyTemplate(state, propertyTemplateKey) 33 | ) 34 | 35 | return newSubjectTemplate 36 | } 37 | 38 | export const selectSubjectTemplateForSubject = (state, subjectKey) => { 39 | const subjectTemplateKey = 40 | state.entities.subjects[subjectKey]?.subjectTemplateKey 41 | return selectSubjectTemplate(state, subjectTemplateKey) 42 | } 43 | 44 | export const selectPropertyTemplateForProperty = (state, propertyKey) => { 45 | const propertyTemplateKey = 46 | state.entities.properties[propertyKey]?.propertyTemplateKey 47 | return selectPropertyTemplate(state, propertyTemplateKey) 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/bootstrap-override.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Stanford University see LICENSE for license */ 2 | 3 | // So that the primary button matches the links, rather than $reno-sand 4 | .btn-primary { 5 | @include button-variant($orient, $orient); 6 | } 7 | 8 | .btn-secondary { 9 | background-color: transparent; 10 | text-decoration: underline; 11 | @include button-outline-variant(map-get($theme-colors, "primary")); 12 | } 13 | 14 | $primary-disabled: lighten(map-get($theme-colors, "secondary"), 20%); 15 | 16 | .btn-primary:disabled { 17 | @include button-variant($primary-disabled, $primary-disabled); 18 | } 19 | 20 | .btn-link { 21 | text-decoration: none; 22 | } 23 | 24 | .btn-link:hover { 25 | text-decoration: underline; 26 | } 27 | 28 | body { 29 | font-family: "Source Sans Pro", sans-serif; 30 | margin: 0 20px; 31 | } 32 | 33 | a { 34 | text-decoration: none; 35 | } 36 | 37 | a:hover { 38 | text-decoration: underline; 39 | } 40 | 41 | pre { 42 | background-color: $white; 43 | border: 1px solid #ccc; 44 | } 45 | 46 | .table-light { 47 | --bs-table-bg: #eaeaea; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/bootstrap-variables.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Stanford University see LICENSE for license */ 2 | 3 | // Boostrap values overrides 4 | // See https://github.com/twbs/bootstrap/blob/main/scss/_variables.scss 5 | 6 | // Colors 7 | $white: #fff; 8 | $blue: $orient; 9 | 10 | // Color Themes 11 | $danger: $bright-red; 12 | 13 | // Options 14 | $enable-shadows: true; 15 | $enable-validation-icons: false; 16 | 17 | // Body 18 | $body-bg: $white-linen; 19 | 20 | // Links 21 | $link-color: $orient; 22 | 23 | // Tables 24 | $table-bg: $white; 25 | 26 | // Forms 27 | $input-bg: $white; 28 | -------------------------------------------------------------------------------- /src/styles/colors.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Stanford University see LICENSE for license */ 2 | 3 | // Color palette: 4 | $orient: #00548f; 5 | $bright-red: #b1020f; 6 | $reno-sand: #b26f16; 7 | $solitude: #e8ecf4; 8 | $spring-wood: #f8f6ef; 9 | $pampas: #f7f4f1; 10 | $white-linen: #f0eae1; 11 | $swirl: #d7cec4; 12 | $double-spanish-white: #cfc2a8; 13 | $nobel: #989898; 14 | $greyblue: #e8ecf4; 15 | -------------------------------------------------------------------------------- /src/styles/diff.scss: -------------------------------------------------------------------------------- 1 | li.add { 2 | list-style-type: "+"; 3 | } 4 | 5 | li.remove { 6 | list-style-type: "-"; 7 | } 8 | 9 | .add { 10 | color: green; 11 | } 12 | 13 | .remove { 14 | color: red; 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/editorheaderbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LD4P/sinopia_editor/d5e3d98ed67908b17ead08d00b3c20c024445bf3/src/styles/editorheaderbg.png -------------------------------------------------------------------------------- /src/styles/editorsinopialogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LD4P/sinopia_editor/d5e3d98ed67908b17ead08d00b3c20c024445bf3/src/styles/editorsinopialogo.png -------------------------------------------------------------------------------- /src/styles/header.scss: -------------------------------------------------------------------------------- 1 | .editor-navbar { 2 | background-image: url("./editorheaderbg.png"); 3 | } 4 | 5 | /** For header tabs **/ 6 | .navbar.editor-navtabs { 7 | color: #2f2424; 8 | 9 | a.nav-link { 10 | padding-left: 1rem; 11 | padding-right: 1rem; 12 | padding-top: 0rem; 13 | } 14 | 15 | a.nav-link.active { 16 | font-weight: 900; 17 | text-decoration: underline; 18 | } 19 | 20 | a.nav-link:hover { 21 | color: #2f2424; 22 | } 23 | 24 | .hover { 25 | background-color: transparent; 26 | } 27 | 28 | a { 29 | color: #2f2424; 30 | font-weight: 500; 31 | } 32 | 33 | #searchType { 34 | width: 19rem; // So that we can fully display "Sinopia BIBFRAME instance resources" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/home-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LD4P/sinopia_editor/d5e3d98ed67908b17ead08d00b3c20c024445bf3/src/styles/home-background.png -------------------------------------------------------------------------------- /src/styles/language.scss: -------------------------------------------------------------------------------- 1 | .btn-lang-clear { 2 | background: transparent escape-svg($btn-close-bg) center / $btn-close-width 3 | auto no-repeat; // include transparent for button elements 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/lookupContext.scss: -------------------------------------------------------------------------------- 1 | .lookup-search-results { 2 | .btn.search-result { 3 | background-color: $pampas; 4 | border-radius: 0; 5 | margin-bottom: 0.15em; 6 | text-align: left; 7 | width: 100%; 8 | 9 | .row { 10 | // necessary for the discogs where we put a grid inside the button. 11 | margin-left: -5px; 12 | margin-right: -5px; 13 | } 14 | } 15 | 16 | /** For typeahead context **/ 17 | .context-container { 18 | padding: 0 0 4px 3px; 19 | 20 | .context-heading { 21 | font-weight: bold; 22 | padding-left: 5px; 23 | } 24 | 25 | .details-container { 26 | padding: 0 0 0 8px; 27 | white-space: normal; 28 | } 29 | 30 | .image-container { 31 | width: 50px; 32 | overflow: hidden; 33 | padding: 3px 0 0; 34 | text-align: center; 35 | } 36 | 37 | .discogs-image { 38 | width: 100%; 39 | margin-right: 10px; 40 | vertical-align: top; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/modal.scss: -------------------------------------------------------------------------------- 1 | .modal-wrapper { 2 | [data-reach-dialog-content] { 3 | padding: 0; 4 | 5 | .card { 6 | background: $spring-wood; 7 | border-color: $nobel; 8 | border-radius: 2px; 9 | } 10 | 11 | .card-header { 12 | background-color: rgba(0, 0, 0, 0); 13 | justify-content: space-between; 14 | align-items: center; 15 | display: flex; 16 | padding-top: 1rem; 17 | padding-bottom: 1rem; 18 | } 19 | 20 | .btn-close { 21 | padding: 0.5rem 0.5rem; 22 | margin: -0.5rem -0.5rem -0.5rem auto; 23 | } 24 | 25 | .card-footer { 26 | background-color: rgba(0, 0, 0, 0); 27 | justify-content: flex-end; 28 | align-items: center; 29 | display: flex; 30 | padding-top: 1rem; 31 | padding-bottom: 1rem; 32 | } 33 | } 34 | } 35 | 36 | .modal-wrapper-lg { 37 | [data-reach-dialog-content] { 38 | width: 75vw; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/relationships.scss: -------------------------------------------------------------------------------- 1 | .relationships-nav { 2 | h5 { 3 | font-weight: bold; 4 | } 5 | 6 | li { 7 | list-style-type: none; 8 | } 9 | 10 | button { 11 | padding-left: 6px; 12 | padding-right: 6px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/resourceEditor.scss: -------------------------------------------------------------------------------- 1 | .resource-header { 2 | background-color: $body-bg; 3 | } 4 | 5 | .sticky-resource-header { 6 | @extend .sticky-top; 7 | background-color: $body-bg; 8 | h3 { 9 | font-size: 1.5rem; 10 | } 11 | } 12 | 13 | .property-uri { 14 | font-style: italic; 15 | font-size: 85%; 16 | } 17 | 18 | .resource-class { 19 | font-style: italic; 20 | font-size: 85%; 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/resourceNav.scss: -------------------------------------------------------------------------------- 1 | .left-nav { 2 | .resource-nav-list-group { 3 | position: sticky; 4 | top: 0; 5 | overflow-y: auto; 6 | height: 100vh; 7 | padding: 0.375rem; 8 | 9 | & > ul { 10 | padding-left: 0; 11 | } 12 | 13 | // Templates have a different color than 14 | .template { 15 | .btn-primary { 16 | background-color: $reno-sand; 17 | border-color: $reno-sand; 18 | } 19 | } 20 | 21 | li { 22 | list-style-type: none; 23 | } 24 | 25 | .left-nav-header { 26 | display: inline; 27 | margin-right: 0.4em; 28 | } 29 | 30 | .current { 31 | color: $body-color; 32 | } 33 | 34 | .property-nav { 35 | @extend .btn; 36 | color: $link-color; 37 | padding: 0.25rem 0.25rem 0.25rem 0.25rem; 38 | text-align: left; 39 | h5 { 40 | font-weight: bold; 41 | } 42 | } 43 | 44 | .fa-circle { 45 | font-size: 60%; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/resourceTabs.scss: -------------------------------------------------------------------------------- 1 | .resources-nav-tabs { 2 | $tab-width: 345px; 3 | $tab-height: 35px; 4 | $button-width: 26px; 5 | $badge-width: 78px; 6 | 7 | margin-bottom: 5px; 8 | margin-top: 10px; 9 | 10 | .nav-item { 11 | border: 1px solid $gray-600; 12 | margin-bottom: 5px; 13 | margin-right: 10px; 14 | height: $tab-height; 15 | width: $tab-width; 16 | 17 | .tab-link { 18 | display: inline-block; 19 | border: 0; 20 | border-radius: 0; 21 | color: $gray-600; 22 | text-decoration: none; 23 | margin: 5px; 24 | 25 | .resource-label { 26 | display: inline-block; 27 | text-overflow: ellipsis; 28 | overflow: hidden; 29 | white-space: nowrap; 30 | width: $tab-width - $button-width - $badge-width - 10px; 31 | } 32 | 33 | .badge { 34 | vertical-align: top; 35 | } 36 | } 37 | } 38 | 39 | .nav-item.active { 40 | border: 0; 41 | background-color: $resource-tab-active-bg; 42 | 43 | .tab-link { 44 | color: $white; 45 | font-weight: 600; 46 | 47 | .resource-label { 48 | width: $tab-width - $badge-width - 10px; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Stanford University see LICENSE for license */ 2 | 3 | // Sinopia variables 4 | $resource-tab-active-bg: $blue; 5 | $heading-icon-color: $blue; 6 | 7 | $prop-heading-bg: $white; 8 | $prop-heading-color: black; 9 | $prop-panel-bg: $white; 10 | 11 | $lookup-value-bg: $swirl; 12 | $lookup-value-border: $nobel; 13 | 14 | $lookup-search-result-bg: $pampas; 15 | 16 | $sidebar-collapse-icon: url("data:image/svg+xml,"); 17 | -------------------------------------------------------------------------------- /src/utilities/Bibframe.js: -------------------------------------------------------------------------------- 1 | export const isBfInstance = (classes) => { 2 | if (!classes) return false 3 | return classes.includes("http://id.loc.gov/ontologies/bibframe/Instance") 4 | } 5 | 6 | export const isBfWork = (classes) => { 7 | if (!classes) return false 8 | return classes.includes("http://id.loc.gov/ontologies/bibframe/Work") 9 | } 10 | 11 | export const isBfItem = (classes) => { 12 | if (!classes) return false 13 | return classes.includes("http://id.loc.gov/ontologies/bibframe/Item") 14 | } 15 | 16 | export const isBfWorkInstanceItem = (classes) => 17 | isBfWork(classes) || isBfInstance(classes) || isBfItem(classes) 18 | 19 | export const isBfAdminMetadata = (classes) => { 20 | if (!classes) return false 21 | return classes.includes("http://id.loc.gov/ontologies/bibframe/AdminMetadata") 22 | } 23 | -------------------------------------------------------------------------------- /src/utilities/Language.js: -------------------------------------------------------------------------------- 1 | export const parseLangTag = (tag) => { 2 | if (!tag) return [null, null, null] 3 | 4 | // This parsing could be more rigorous, but starting with simplest approach. 5 | const splitLang = tag.split("-") 6 | const langSubtag = splitLang[0] 7 | const scriptSubtag = parseScriptTag(splitLang) 8 | const transliterationSubtag = parseTransliterationSubtag(splitLang) 9 | 10 | return [langSubtag, scriptSubtag, transliterationSubtag] 11 | } 12 | 13 | export const stringifyLangTag = (langCode, scriptCode, transliterationCode) => { 14 | if (!langCode) return null 15 | const subtags = [langCode] 16 | if (scriptCode) subtags.push(scriptCode) 17 | if (transliterationCode) 18 | subtags.push(`t-${langCode}-m0-${transliterationCode}`) 19 | 20 | return subtags.join("-") 21 | } 22 | 23 | const startsWithUpperCase = (str) => 24 | str.charAt(0) === str.charAt(0).toUpperCase() 25 | 26 | const parseScriptTag = (splitLang) => { 27 | if (!splitLang[1]) return null 28 | if (!startsWithUpperCase(splitLang[1])) return null 29 | 30 | return splitLang[1] 31 | } 32 | 33 | const parseTransliterationSubtag = (splitLang) => { 34 | // For example, ja-Latn-t-ja-m0-alaloc 35 | const matchPos = [1, 2].find( 36 | (pos) => 37 | splitLang[pos] === "t" && 38 | splitLang[pos + 2] === "m0" && 39 | !startsWithUpperCase(splitLang[pos + 3]) 40 | ) 41 | return matchPos ? splitLang[matchPos + 3] : null 42 | } 43 | 44 | export const chooseLang = (isSuppressed, defaultResourceLang) => 45 | isSuppressed ? null : defaultResourceLang 46 | -------------------------------------------------------------------------------- /src/utilities/Search.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Stanford University see LICENSE for license 2 | 3 | import Config from "Config" 4 | 5 | export const defaultSearchResultsPerPage = (searchType) => 6 | searchType === "template" 7 | ? Config.templateSearchResultsPerPage 8 | : Config.searchResultsPerPage 9 | 10 | export const noop = () => {} 11 | -------------------------------------------------------------------------------- /src/utilities/authorityConfig.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Stanford University see LICENSE for license 2 | 3 | import authorityConfig from "../../static/authorityConfig.json" 4 | 5 | const authorityConfigMap = {} 6 | authorityConfig.forEach( 7 | (configItem) => (authorityConfigMap[configItem.uri] = configItem) 8 | ) 9 | 10 | export const findAuthorityConfig = (searchUri) => authorityConfigMap[searchUri] 11 | 12 | export const sinopiaSearchUri = "urn:ld4p:sinopia" 13 | -------------------------------------------------------------------------------- /src/utilities/errorKeyFactory.js: -------------------------------------------------------------------------------- 1 | export const resourceEditErrorKey = (resourceKey) => 2 | `resourceedit-${resourceKey}` 3 | 4 | export const dashboardErrorKey = "dashboard" 5 | 6 | export const templateErrorKey = "template" 7 | 8 | export const exportsErrorKey = "exports" 9 | 10 | export const signInErrorKey = "signin" 11 | 12 | export const searchQARetrieveErrorKey = "searchqaresource" 13 | 14 | export const searchErrorKey = "search" 15 | 16 | export const metricsErrorKey = "metrics" 17 | -------------------------------------------------------------------------------- /static/literalDataType.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "http://www.w3.org/2001/XMLSchema#integer", 4 | "label": "Integer (xsd:integer)" 5 | }, 6 | { 7 | "uri": "http://www.w3.org/2001/XMLSchema#dateTime", 8 | "label": "Date and time with or without timezone (xsd:dateTime)" 9 | }, 10 | { 11 | "uri": "http://www.w3.org/2001/XMLSchema#dateTimeStamp", 12 | "label": "Date and time with required timezone (xsd:dateTimeStamp)" 13 | }, 14 | { 15 | "uri": "http://id.loc.gov/datatypes/edtf", 16 | "label": "Extended Date/Time Format (EDTF)" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /static/literalPropertyAttribute.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "http://sinopia.io/vocabulary/literalPropertyAttribute/userIdDefault", 4 | "label": "user ID default" 5 | }, 6 | { 7 | "uri": "http://sinopia.io/vocabulary/literalPropertyAttribute/dateDefault", 8 | "label": "date default" 9 | } 10 | 11 | ] 12 | -------------------------------------------------------------------------------- /static/propertyAttribute.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "http://sinopia.io/vocabulary/propertyAttribute/repeatable", 4 | "label": "repeatable" 5 | }, 6 | { 7 | "uri": "http://sinopia.io/vocabulary/propertyAttribute/required", 8 | "label": "required" 9 | }, 10 | { 11 | "uri": "http://sinopia.io/vocabulary/propertyAttribute/ordered", 12 | "label": "ordered" 13 | }, 14 | { 15 | "uri": "http://sinopia.io/vocabulary/propertyAttribute/languageSuppressed", 16 | "label": "language suppressed" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /static/propertyType.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "http://sinopia.io/vocabulary/propertyType/literal", 4 | "label": "literal" 5 | }, 6 | { 7 | "uri": "http://sinopia.io/vocabulary/propertyType/uri", 8 | "label": "uri or lookup" 9 | }, 10 | { 11 | "uri": "http://sinopia.io/vocabulary/propertyType/resource", 12 | "label": "nested resource" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /static/resourceAttribute.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "http://sinopia.io/vocabulary/resourceAttribute/suppressible", 4 | "label": "suppressible" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /static/searchConfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "DISCOGS Masters", 4 | "uri": "urn:discogs:master" 5 | }, 6 | { 7 | "label": "DISCOGS Releases", 8 | "uri": "urn:discogs:release" 9 | }, 10 | { 11 | "label": "SHAREVDE PCC Opus", 12 | "uri": "urn:ld4p:qa:sharevde_pcc_ld4l_cache:opus" 13 | }, 14 | { 15 | "label": "SHAREVDE PCC Work", 16 | "uri": "urn:ld4p:qa:sharevde_pcc_ld4l_cache:work" 17 | }, 18 | { 19 | "label": "SHAREVDE PCC Instance", 20 | "uri": "urn:ld4p:qa:sharevde_pcc_ld4l_cache:instance" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /static/uriAttribute.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "http://sinopia.io/vocabulary/uriAttribute/labelSuppressed", 4 | "label": "label suppressed" 5 | } 6 | ] 7 | --------------------------------------------------------------------------------