"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 |
38 | {" "}
39 | Copied!
40 |
41 | )
42 |
43 | return (
44 |
51 | Copy {label}
52 |
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 |
19 | {long}
20 |
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 |
18 |
19 |
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 |
18 |
19 |
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 |
5 |
10 |
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 |
24 |
25 |
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 |
18 |
19 |
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 | Label / ID
24 | Class
25 | Group
26 | Modified
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 |
23 | props.handleSearch(props.row.query, props.row.authorityUri, e)
24 | }
25 | >
26 |
27 |
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:
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 |
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 |
23 |
27 |
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 |
36 |
37 |
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 |
42 | Permissions
43 |
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 |
25 |
26 |
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 |
21 | Top
22 |
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 |
28 | {btnText}
29 |
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 |
33 | {props.character}
34 |
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 |
13 | {specialcharacters[key].label}
14 |
15 | )
16 | })
17 | return options
18 | }
19 |
20 | const handleChange = (event) => {
21 | props.selectVocabulary(event)
22 | }
23 |
24 | return (
25 |
35 | {getOptions()}
36 |
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 |
13 | ä
14 |
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 |
32 | {value.lang || "No language specified"}
33 |
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 |
14 |
15 |
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 |
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 |
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 |
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 |
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 |
35 |
36 |
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 |
22 | Search for template
23 |
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 |
23 | Selected template ID
24 |
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 |
20 | {label}
21 |
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 |
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 | Label / ID
22 | Resource URI
23 | Author
24 | Group
25 | Date
26 | Guiding statement
27 |
28 | Actions
29 |
30 |
31 |
32 | {rows}
33 |
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 |
--------------------------------------------------------------------------------