@cu-mkp/editioncrafter 1.3.0 → 1.3.1-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,43 @@
1
- import { Box, Button, ButtonGroup, Collapse, Divider, IconButton, Typography } from '@material-ui/core'
2
- import { red } from '@material-ui/core/colors'
1
+ import { Box, Button, Collapse, Divider, IconButton, Input, Typography } from '@material-ui/core'
3
2
  import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
4
- import GridOnIcon from '@material-ui/icons/GridOn'
5
- import ListIcon from '@material-ui/icons/List'
6
3
  import TuneIcon from '@material-ui/icons/Tune'
7
- import { useEffect, useMemo, useState } from 'react'
4
+ import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
8
5
 
9
6
  import { useLocation, useNavigate } from 'react-router-dom'
10
7
  import { getObjs } from '../../common/lib/sql'
11
8
  import DocumentDetail from './DocumentDetail'
9
+ // import DocumentFilters from './DocumentFilters'
10
+ import TagFilterContext from '../../EditionCrafter/context/TagFilterContext'
12
11
  import TagFilters from './TagFilters'
13
12
 
14
13
  function getData(db) {
15
14
  const docStmt = db.prepare(`
16
15
  SELECT
17
- documents.id AS id,
18
- documents.name AS name,
19
- documents.local_id AS local_id
16
+ d.id AS id,
17
+ d.name AS name,
18
+ d.local_id AS local_id,
19
+ json_group_array(document_taggings.tag) as tags
20
20
  FROM
21
- documents
21
+ documents d
22
+ LEFT JOIN document_taggings ON d.id = document_taggings.document
23
+ GROUP BY d.id, d.name, d.local_id
22
24
  `)
23
-
24
25
  return getObjs(docStmt)
25
26
  }
26
27
 
28
+ function getDocTags(db) {
29
+ const tagsStmt = db.prepare(`
30
+ SELECT
31
+ tags.id AS id
32
+ FROM
33
+ tags
34
+ LEFT JOIN taxonomies ON tags.taxonomy_id = taxonomies.id
35
+ WHERE
36
+ taxonomies.is_surface = 0 OR taxonomies.is_surface IS NULL
37
+ `)
38
+ return getObjs(tagsStmt)
39
+ }
40
+
27
41
  function parseFolioID(folioID) {
28
42
  if (!folioID) {
29
43
  return null
@@ -50,27 +64,48 @@ function getSelection(path) {
50
64
  function SurfaceBrowser(props) {
51
65
  const { db, open, toggleOpen } = props
52
66
  const documents = useMemo(() => getData(db), [db])
67
+ const docTags = useMemo(() => getDocTags(db)?.map(tag => (tag.id)), [db])
53
68
  const [pageCount, setPageCount] = useState({})
54
69
  const [totalPages, setTotalPages] = useState(0)
55
70
  const [tags, setTags] = useState([])
56
71
  const [showFilters, setShowFilters] = useState(false)
72
+ const [query, setQuery] = useState(undefined)
73
+
74
+ const filterDocs = useCallback((docs, tags) => {
75
+ return docs.filter((doc) => {
76
+ const docID = doc.id
77
+ for (const tag of tags) {
78
+ if (docTags.includes(tag) && !JSON.parse(doc.tags)?.includes(tag)) {
79
+ const newCount = pageCount
80
+ newCount[docID] = 0
81
+ setPageCount(newCount)
82
+ return false
83
+ }
84
+ }
85
+ return true
86
+ })
87
+ }, [docTags, pageCount])
88
+
89
+ const filteredDocs = useMemo(() => (filterDocs(documents, tags)), [filterDocs, documents, tags])
90
+
91
+ const { clearTags } = useContext(TagFilterContext)
57
92
 
58
93
  const navigate = useNavigate()
59
94
  const location = useLocation()
60
95
  const selection = useMemo(() => getSelection(location.pathname), [location])
61
96
 
62
- const navigateToSelection = (nextSelection) => {
97
+ const navigateToSelection = useCallback((nextSelection) => {
63
98
  const folioID = nextSelection?.left ? `${nextSelection.left.localID}_${nextSelection.left.surfaceID}` : null
64
99
  const folioID2 = nextSelection?.right ? `${nextSelection.right.localID}_${nextSelection.right.surfaceID}` : null
65
100
  const navParams = `/ec/${folioID || '-1'}/${folioID ? 'f' : 'g'}/${folioID2 || '-1'}/${folioID2 ? 'f' : 'g'}`
66
101
  navigate(navParams + location.search)
67
- }
102
+ }, [location.search, navigate])
68
103
 
69
- const updatePageCount = (documentID, numPages) => {
104
+ const updatePageCount = useCallback((documentID, numPages) => {
70
105
  const newCount = pageCount
71
106
  newCount[documentID] = numPages
72
107
  setPageCount(newCount)
73
- }
108
+ }, [pageCount])
74
109
 
75
110
  useEffect(() => {
76
111
  let p = 0
@@ -80,7 +115,7 @@ function SurfaceBrowser(props) {
80
115
  setTotalPages(p)
81
116
  }, [pageCount, tags])
82
117
 
83
- const documentDetails = documents.map((doc) => {
118
+ const documentDetails = useMemo(() => filteredDocs.map((doc) => {
84
119
  return (
85
120
  <DocumentDetail
86
121
  key={`document-detail-${doc.id}`}
@@ -91,11 +126,11 @@ function SurfaceBrowser(props) {
91
126
  selection={selection}
92
127
  navigateToSelection={navigateToSelection}
93
128
  updatePageCount={count => updatePageCount(doc.id, count)}
94
- tags={tags}
129
+ tags={tags?.filter(tag => !docTags.includes(tag))}
95
130
  >
96
131
  </DocumentDetail>
97
132
  )
98
- })
133
+ }), [db, docTags, filteredDocs, navigateToSelection, selection, tags, updatePageCount])
99
134
 
100
135
  return (
101
136
  <Collapse in={open} horizontal>
@@ -108,34 +143,51 @@ function SurfaceBrowser(props) {
108
143
  <Divider></Divider>
109
144
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
110
145
  <Typography>Contents</Typography>
111
- <Button
112
- startIcon={<TuneIcon />}
113
- onClick={() => setShowFilters(current => (!current))}
114
- >
115
- Filter
146
+ <div>
116
147
  { tags && tags.length
117
148
  ? (
118
- <div style={{
119
- fontSize: 'small',
120
- backgroundColor: 'red',
121
- borderRadius: '999px',
122
- display: 'flex',
123
- justifyContent: 'center',
124
- alignItems: 'center',
125
- padding: '3px',
126
- color: 'white',
127
- height: '16px',
128
- width: '16px',
129
- position: 'absolute',
130
- top: '0',
131
- left: '-12px',
132
- }}
149
+ <Button
150
+ onClick={() => {
151
+ setTags([])
152
+ clearTags()
153
+ }}
154
+ style={{
155
+ marginRight: '24px',
156
+ }}
133
157
  >
134
- {tags.length}
135
- </div>
158
+ Clear All
159
+ </Button>
136
160
  )
137
161
  : null}
138
- </Button>
162
+ <Button
163
+ startIcon={<TuneIcon />}
164
+ onClick={() => setShowFilters(current => (!current))}
165
+ >
166
+ Filter
167
+ { tags && tags.length
168
+ ? (
169
+ <div style={{
170
+ fontSize: 'small',
171
+ backgroundColor: 'red',
172
+ borderRadius: '999px',
173
+ display: 'flex',
174
+ justifyContent: 'center',
175
+ alignItems: 'center',
176
+ padding: '3px',
177
+ color: 'white',
178
+ height: '16px',
179
+ width: '16px',
180
+ position: 'absolute',
181
+ top: '0',
182
+ left: '-12px',
183
+ }}
184
+ >
185
+ {tags.length}
186
+ </div>
187
+ )
188
+ : null}
189
+ </Button>
190
+ </div>
139
191
  </div>
140
192
  <Typography>
141
193
  {totalPages}
@@ -152,18 +204,22 @@ function SurfaceBrowser(props) {
152
204
  </IconButton>
153
205
  </ButtonGroup> */}
154
206
  { showFilters && (
155
- <TagFilters
156
- db={db}
157
- filters={tags}
158
- onToggleSelected={(tagId) => {
159
- if (tags.includes(tagId)) {
160
- setTags(current => (current.filter(t => (t !== tagId))))
161
- }
162
- else {
163
- setTags(current => ([...current, tagId]))
164
- }
165
- }}
166
- />
207
+ <div className="tag-filters">
208
+ <Input placeholder="Search for filters" value={query} onChange={(e) => { setQuery(e.target.value) }} className="tag-filters-search" />
209
+ <TagFilters
210
+ db={db}
211
+ filters={tags}
212
+ query={query}
213
+ onToggleSelected={(tagId) => {
214
+ if (tags.includes(tagId)) {
215
+ setTags(current => (current.filter(t => (t !== tagId))))
216
+ }
217
+ else {
218
+ setTags(current => ([...current, tagId]))
219
+ }
220
+ }}
221
+ />
222
+ </div>
167
223
  ) }
168
224
  <Box className="surface-browser-document-details">
169
225
  { documentDetails }
@@ -1,5 +1,6 @@
1
- import { Checkbox, FormControlLabel, FormGroup, Typography } from '@material-ui/core'
2
- import { useContext, useEffect, useMemo, useState } from 'react'
1
+ import { Accordion, AccordionDetails, AccordionSummary, Checkbox, FormControlLabel, FormGroup, Typography } from '@material-ui/core'
2
+ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
3
+ import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
3
4
  import { getObjs } from '../../common/lib/sql'
4
5
  import TagFilterContext from '../../EditionCrafter/context/TagFilterContext'
5
6
 
@@ -9,6 +10,17 @@ function getData(db) {
9
10
  *
10
11
  FROM
11
12
  taxonomies;
13
+ ORDER BY
14
+ name ASC;
15
+ `)
16
+
17
+ const categoriesStmt = db.prepare(`
18
+ SELECT
19
+ *
20
+ FROM
21
+ categories
22
+ ORDER BY
23
+ name ASC;
12
24
  `)
13
25
 
14
26
  const tagsStmt = db.prepare(`
@@ -16,90 +28,234 @@ function getData(db) {
16
28
  tags.id AS id,
17
29
  tags.name AS name,
18
30
  tags.xml_id AS xml_id,
31
+ tags.parent_category_id AS parent_category_id,
19
32
  taxonomies.name as taxonomy,
20
33
  taxonomies.id as taxonomy_id
21
34
  FROM
22
35
  tags
23
36
  LEFT JOIN taxonomies
24
37
  ON tags.taxonomy_id = taxonomies.id
25
- INNER JOIN taggings
26
- ON taggings.tag_id = tags.id
27
- INNER JOIN elements
28
- ON elements.id = taggings.element_id
29
- WHERE
30
- elements.type = 'zone'
31
38
  GROUP BY
32
- tags.xml_id`)
39
+ tags.xml_id
40
+ ORDER BY
41
+ tags.name ASC`)
33
42
 
34
43
  return {
35
44
  tags: getObjs(tagsStmt),
45
+ categories: getObjs(categoriesStmt),
36
46
  taxonomies: getObjs(taxonomiesStmt),
37
47
  }
38
48
  }
39
49
 
50
+ function CategoryFilter(props) {
51
+ const {
52
+ name,
53
+ categoryId,
54
+ tags,
55
+ categories,
56
+ toggleTag,
57
+ onToggleSelected,
58
+ isSurface,
59
+ filters,
60
+ } = props
61
+
62
+ const hasDescendentTags = useCallback((catId) => {
63
+ if (tags?.filter(tag => (tag.parent_category_id === catId))?.length) {
64
+ return true
65
+ }
66
+
67
+ const subs = categories?.filter(cat => (cat.parent_category_id === catId))
68
+
69
+ for (const sub of subs) {
70
+ if (hasDescendentTags(sub.id)) {
71
+ return true
72
+ }
73
+ }
74
+
75
+ return false
76
+ }, [tags, categories])
77
+
78
+ const countDescendentTags = useCallback((catId) => {
79
+ let count = tags?.filter(tag => (tag.parent_category_id === catId))?.length
80
+
81
+ const subs = categories?.filter(cat => (cat.parent_category_id === catId))
82
+
83
+ for (const sub of subs) {
84
+ count = count + countDescendentTags(sub.id)
85
+ }
86
+
87
+ return count
88
+ }, [tags, categories])
89
+
90
+ const categoryTags = useMemo(() => {
91
+ return tags?.filter(tag => (tag.parent_category_id === categoryId))
92
+ }, [tags, categoryId])
93
+
94
+ const subcategories = useMemo(() => {
95
+ return categories?.filter(cat => (cat.parent_category_id === categoryId))
96
+ }, [categories, categoryId])
97
+
98
+ return hasDescendentTags(categoryId) && (
99
+ <Accordion>
100
+ <AccordionSummary
101
+ expandIcon={<ExpandMoreIcon />}
102
+ aria-controls={`category-tags-${name}-content`}
103
+ id={`category-tags-${name}`}
104
+ className="accordion-summary"
105
+ >
106
+ <Typography>{name}</Typography>
107
+ <Typography>{countDescendentTags(categoryId) || ''}</Typography>
108
+ </AccordionSummary>
109
+ <AccordionDetails
110
+ className="accordion-detail"
111
+ >
112
+ { !!categoryTags?.length && (
113
+ <ul>
114
+ { categoryTags?.map(tag => (
115
+ <FormControlLabel
116
+ as="li"
117
+ control={(
118
+ <Checkbox
119
+ checked={filters.includes(tag.id)}
120
+ onChange={() => {
121
+ onToggleSelected(tag.id)
122
+ if (isSurface) {
123
+ toggleTag(tag.xml_id, 'left')
124
+ toggleTag(tag.xml_id, 'right')
125
+ }
126
+ }}
127
+ />
128
+ )}
129
+ key={tag.id}
130
+ label={tag.name}
131
+ />
132
+ ))}
133
+ </ul>
134
+ )}
135
+ {
136
+ !!subcategories?.length && (
137
+ <>
138
+ {
139
+ subcategories.map(cat => (
140
+ <CategoryFilter
141
+ key={cat.id}
142
+ name={cat.name}
143
+ categoryId={cat.id}
144
+ tags={tags}
145
+ categories={categories}
146
+ toggleTag={toggleTag}
147
+ onToggleSelected={onToggleSelected}
148
+ isSurface={isSurface}
149
+ filters={filters}
150
+ />
151
+ ))
152
+ }
153
+ </>
154
+ )
155
+ }
156
+ </AccordionDetails>
157
+ </Accordion>
158
+ )
159
+ }
160
+
40
161
  function TagFilters(props) {
41
- const { onToggleSelected, filters } = props
162
+ const { onToggleSelected, filters, query } = props
42
163
  const data = useMemo(() => getData(props.db), [props.db])
43
- const [expanded, setExpanded] = useState(data.taxonomies?.map(() => (false)))
44
164
  const [displayedTags, setDisplayedTags] = useState({})
45
165
 
46
166
  const { toggleTag } = useContext(TagFilterContext)
47
167
 
48
168
  useEffect(() => {
49
169
  const tags = {}
170
+ const filteredTags = data.tags.filter(tag => (!query || !query.length || tag.name.toLowerCase().includes(query.toLowerCase())))
50
171
  for (let i = 0; i < data.taxonomies.length; i++) {
51
172
  const tax = data.taxonomies[i]
52
- const tagList = expanded[i] ? data.tags.filter(t => (t.taxonomy_id === tax.id)) : data.tags.filter(t => (t.taxonomy_id === tax.id))?.slice(0, 5)
53
- tags[tax.id] = tagList
173
+ const tagList = filteredTags.filter(t => t.taxonomy_id === tax.id)
174
+ if (tagList?.length) {
175
+ tags[tax.id] = tagList
176
+ }
54
177
  }
55
178
  setDisplayedTags(tags)
56
- }, [expanded, data])
179
+ }, [data, query])
57
180
 
58
181
  return (
59
- <div className="tag-filters">
60
- <div className="tag-list">
61
- <FormGroup>
62
- { data.taxonomies.map((tax, idx) => {
63
- const tagList = displayedTags[tax.id]
64
- return (
65
- <div key={tax.id}>
66
- <Typography>{tax.name}</Typography>
67
- <ul>
68
- { tagList?.map(tag => (
69
- <FormControlLabel
70
- as="li"
71
- control={(
72
- <Checkbox
73
- checked={filters.includes(tag.id)}
74
- onChange={() => {
75
- onToggleSelected(tag.id)
76
- toggleTag(tag.xml_id, 'left')
77
- toggleTag(tag.xml_id, 'right')
78
- }}
79
- />
80
- )}
81
- key={tag.id}
82
- label={tag.name}
83
- />
84
- ))}
85
- </ul>
86
- <button
87
- className="tag-filter-button"
88
- type="button"
89
- onClick={() => {
90
- const newState = [...expanded]
91
- newState[idx] = !expanded[idx]
92
- setExpanded(newState)
93
- }}
94
- >
95
- { !data.tags.filter(t => (t.taxonomy_id === tax.id))?.length || data.tags.filter(t => (t.taxonomy_id === tax.id)).length < 6 ? null : expanded[idx] ? 'Show less' : 'Show more'}
96
- </button>
182
+ <>
183
+ {
184
+ data?.tags?.length
185
+ ? (
186
+ <div className="tag-list">
187
+ <FormGroup>
188
+ { data.taxonomies.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 0)).map((tax) => {
189
+ const tagList = displayedTags[tax.id]
190
+ const topLevelTags = tagList?.filter(tag => (!tag.parent_category_id))
191
+ return (
192
+ tagList?.length
193
+ ? (
194
+ <Accordion>
195
+ <AccordionSummary
196
+ expandIcon={<ExpandMoreIcon />}
197
+ aria-controls={`tags-${tax.name}-content`}
198
+ id={`tags-${tax.name}`}
199
+ className="accordion-summary"
200
+ >
201
+ <Typography>{`${tax.name.slice(0, 1).toUpperCase()}${tax.name.slice(1)}`}</Typography>
202
+ <Typography>{tagList?.length || ''}</Typography>
203
+ </AccordionSummary>
204
+ <AccordionDetails
205
+ className="accordion-detail"
206
+ >
207
+ <div key={tax.id}>
208
+ { !!topLevelTags?.length && (
209
+ <ul>
210
+ { topLevelTags?.map(tag => (
211
+ <FormControlLabel
212
+ as="li"
213
+ control={(
214
+ <Checkbox
215
+ checked={filters.includes(tag.id)}
216
+ onChange={() => {
217
+ onToggleSelected(tag.id)
218
+ if (tax.is_surface) {
219
+ toggleTag(tag.xml_id, 'left')
220
+ toggleTag(tag.xml_id, 'right')
221
+ }
222
+ }}
223
+ />
224
+ )}
225
+ key={tag.id}
226
+ label={tag.name}
227
+ />
228
+ ))}
229
+ </ul>
230
+ )}
231
+ {
232
+ data.categories?.filter(cat => (cat.taxonomy_id === tax.id && !cat.parent_category_id))?.map(cat => (
233
+ <CategoryFilter
234
+ key={cat.id}
235
+ name={cat.name}
236
+ categoryId={cat.id}
237
+ tags={tagList}
238
+ categories={data.categories}
239
+ toggleTag={toggleTag}
240
+ onToggleSelected={onToggleSelected}
241
+ isSurface={tax.is_surface}
242
+ filters={filters}
243
+ />
244
+ ))
245
+ }
246
+ </div>
247
+ </AccordionDetails>
248
+ </Accordion>
249
+ )
250
+ : null
251
+ )
252
+ })}
253
+ </FormGroup>
97
254
  </div>
98
255
  )
99
- })}
100
- </FormGroup>
101
- </div>
102
- </div>
256
+ : null
257
+ }
258
+ </>
103
259
  )
104
260
  }
105
261
 
@@ -48,14 +48,14 @@ function getData(db) {
48
48
 
49
49
  function generateECProps(props, db) {
50
50
  const documents = getData(db)
51
- const { documentName, baseURL, transcriptionTypes } = props
51
+ const { documentName, baseURL, transcriptionTypes, manifestPath = '/iiif/manifest.json' } = props
52
52
  const documentInfo = {}
53
53
 
54
54
  for (const document of documents) {
55
55
  documentInfo[document.local_id] = {
56
56
  documentName: document.name,
57
57
  transcriptionTypes,
58
- iiifManifest: `${baseURL}/${document.local_id}/iiif/manifest.json`,
58
+ iiifManifest: `${baseURL}/${document.local_id}${manifestPath}`,
59
59
  }
60
60
  }
61
61
 
@@ -26,13 +26,18 @@
26
26
  overflow-y: auto;
27
27
  position: absolute;
28
28
  top: 48px;
29
- left: 0;
29
+ left: 400px;
30
30
  background-color: #242629;
31
31
  z-index: 10000;
32
32
  padding: 12px 0 12px 12px;
33
- width: 100%;
33
+ width: 450px;
34
34
  height: 100%;
35
- max-height: 100dvh;
35
+ max-height: calc(97dvh - 48px);
36
+ border-left: solid white 1px;
37
+ }
38
+
39
+ .tag-explore .tag-filters .tag-filters-search {
40
+ margin-bottom: 24px;
36
41
  }
37
42
 
38
43
  .tag-explore .tag-filter-button {
@@ -153,8 +158,8 @@
153
158
  position: absolute;
154
159
  top: 0;
155
160
  left: 48px;
156
- height: 100vh;
157
- min-width: 300px;
161
+ height: 97dvh;
162
+ width: 400px;
158
163
  display: flex;
159
164
  flex-direction: column;
160
165
  background-color: #242629;
@@ -189,4 +194,6 @@
189
194
 
190
195
  .tag-explore .accordion-detail {
191
196
  background-color: #242629;
197
+ display: flex;
198
+ flex-direction: column;
192
199
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cu-mkp/editioncrafter",
3
3
  "type": "module",
4
- "version": "1.3.0",
4
+ "version": "1.3.1-beta.10",
5
5
  "private": false,
6
6
  "description": "A simple digital critical edition publication tool",
7
7
  "license": "MIT",