@asd20/ui-next 2.1.0 → 2.2.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ # [2.2.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.1.0...ui-next-v2.2.0) (2026-04-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * fix plublishing contract regressions ([9ce287b](https://github.com/academydistrict20/asd20-ui-next/commit/9ce287b8f5719db6fdf1467fe4423fc60bbbc19c))
9
+
10
+
11
+ ### Features
12
+
13
+ * add pagination to file list template ([cf9829c](https://github.com/academydistrict20/asd20-ui-next/commit/cf9829cd4776e2fd0c24cee52092397cdc6a16e8))
14
+
3
15
  # [2.1.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.29...ui-next-v2.1.0) (2026-04-06)
4
16
 
5
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -118,6 +118,27 @@ import {
118
118
  shouldShowExternalIcon,
119
119
  } from '../../../helpers/linkPolicy'
120
120
 
121
+ function resolveRouter(vm) {
122
+ const internal = vm && vm.$
123
+ const context = internal && internal.ctx
124
+ if (context && context.$router) {
125
+ return context.$router
126
+ }
127
+
128
+ const parentContext = internal && internal.parent && internal.parent.ctx
129
+ if (parentContext && parentContext.$router) {
130
+ return parentContext.$router
131
+ }
132
+
133
+ return (
134
+ internal &&
135
+ internal.appContext &&
136
+ internal.appContext.config &&
137
+ internal.appContext.config.globalProperties &&
138
+ internal.appContext.config.globalProperties.$router
139
+ )
140
+ }
141
+
121
142
  export default {
122
143
  name: 'Asd20ListItem',
123
144
 
@@ -183,7 +204,8 @@ export default {
183
204
  return this.link || this.to
184
205
  },
185
206
  usesRouterNavigation() {
186
- if (!this.$router || !this.to) return false
207
+ const router = resolveRouter(this)
208
+ if (!router || !this.to) return false
187
209
  if (typeof this.to === 'string') return this.to.startsWith('/')
188
210
  return typeof this.to === 'object'
189
211
  },
@@ -206,9 +228,11 @@ export default {
206
228
 
207
229
  methods: {
208
230
  onClick(e) {
209
- if (this.usesRouterNavigation) {
231
+ const router = resolveRouter(this)
232
+
233
+ if (this.usesRouterNavigation && router && typeof router.push === 'function') {
210
234
  e.preventDefault()
211
- this.$router.push(this.to)
235
+ router.push(this.to)
212
236
  }
213
237
  this.$emit('click', e)
214
238
  },
@@ -28,10 +28,10 @@
28
28
  @input="input"
29
29
  />
30
30
  <div
31
- v-if="extra || !!$slots.default"
31
+ v-if="extra"
32
32
  class="asd20-search-field__extra"
33
33
  >
34
- {{ extra }}<slot />
34
+ {{ extra }}
35
35
  </div>
36
36
  </div>
37
37
  </template>
@@ -141,35 +141,14 @@ export default {
141
141
  opacity: 1;
142
142
  color: var(--color__primary-t50);
143
143
  }
144
- &:focus + .asd20-search-field__extra {
145
- background: var(--website-page__alternate-background-t40);
146
- }
147
144
  }
148
145
 
149
- .asd20-search-field__extra {
150
- transition: background asd20-speed(1) ease;
151
- align-self: stretch;
152
- display: flex;
153
- align-items: center;
154
- justify-content: center;
155
- font-size: 0.875rem;
146
+ &__extra {
147
+ flex-shrink: 0;
148
+ padding: 0 space(0.5);
149
+ color: var(--color__primary);
150
+ font-size: space(0.5);
156
151
  white-space: nowrap;
157
- color: var(--color__primary-t50);
158
- box-shadow: -1px 0 0 0 var(--color__tertiary);
159
- }
160
- &--large {
161
- & > .asd20-icon {
162
- width: space(1.5);
163
- height: space(1.5);
164
- margin: 0;
165
- }
166
- input {
167
- margin-left: space(0);
168
- font-size: 2rem;
169
- line-height: 1.5;
170
- padding: 0 space(0.25) 0 space(0.5);
171
- height: space(3);
172
- }
173
152
  }
174
153
  }
175
154
  </style>
@@ -24,6 +24,49 @@
24
24
  />
25
25
  </div>
26
26
  </template>
27
+ <template #after-header>
28
+ <div
29
+ v-if="showTopPagination"
30
+ class="asd20-file-list__pagination"
31
+ >
32
+ <p class="asd20-file-list__pagination-summary">
33
+ {{ paginationSummary }}
34
+ </p>
35
+ <div
36
+ v-if="paginate && totalPages > 1"
37
+ class="asd20-file-list__pagination-actions"
38
+ >
39
+ <asd20-button
40
+ class="asd20-file-list__pagination-button"
41
+ icon="chevron"
42
+ label="Prev"
43
+ :icon-angle="-90"
44
+ size="md"
45
+ horizontal
46
+ transparent
47
+ centered
48
+ opposite
49
+ :disabled="currentPage === 1"
50
+ @click="prevPage"
51
+ />
52
+ <p class="asd20-file-list__pagination-page">
53
+ Page {{ currentPage }} of {{ totalPages }}
54
+ </p>
55
+ <asd20-button
56
+ class="asd20-file-list__pagination-button"
57
+ icon="chevron"
58
+ label="Next"
59
+ :icon-angle="90"
60
+ size="md"
61
+ horizontal
62
+ transparent
63
+ centered
64
+ :disabled="currentPage === totalPages"
65
+ @click="nextPage"
66
+ />
67
+ </div>
68
+ </div>
69
+ </template>
27
70
  <div
28
71
  v-for="category in categorizedFileItems"
29
72
  :key="category.name"
@@ -74,6 +117,7 @@ import Asd20List from '../../../components/organisms/Asd20WidgetList'
74
117
  import mapFilesToListItems from '../../../helpers/mapFilesToListItems'
75
118
  import Asd20ListItem from '../../../components/molecules/Asd20ListItem'
76
119
  import Asd20ListCategory from '../../../components/molecules/Asd20ListCategory'
120
+ import Asd20Button from '../../../components/atoms/Asd20Button'
77
121
  import Asd20SearchField from '../../../components/molecules/Asd20SearchField'
78
122
 
79
123
  function normalizeSearchText(value = '') {
@@ -88,6 +132,7 @@ export default {
88
132
  name: 'Asd20FileList',
89
133
 
90
134
  components: {
135
+ Asd20Button,
91
136
  Asd20List,
92
137
  Asd20ListItem,
93
138
  Asd20ListCategory,
@@ -107,6 +152,8 @@ export default {
107
152
  maxHeight: { type: String, default: '320px' },
108
153
  searchable: { type: Boolean, default: false },
109
154
  searchPlaceholder: { type: String, default: 'Search files' },
155
+ paginate: { type: Boolean, default: false },
156
+ pageSize: { type: Number, default: 50 },
110
157
  initialVisibleCount: { type: Number, default: 0 },
111
158
  visibleCountStep: { type: Number, default: 50 },
112
159
  },
@@ -114,6 +161,7 @@ export default {
114
161
  data: () => ({
115
162
  loadedFiles: [],
116
163
  searchQuery: '',
164
+ currentPage: 1,
117
165
  visibleCount: 0,
118
166
  }),
119
167
 
@@ -146,6 +194,25 @@ export default {
146
194
  return this.filteredFiles.length
147
195
  },
148
196
 
197
+ resolvedPageSize() {
198
+ return this.pageSize > 0 ? this.pageSize : 50
199
+ },
200
+
201
+ totalPages() {
202
+ if (!this.paginate || !this.filteredFilesCount) return 1
203
+ return Math.max(1, Math.ceil(this.filteredFilesCount / this.resolvedPageSize))
204
+ },
205
+
206
+ paginationStart() {
207
+ if (!this.filteredFilesCount) return 0
208
+ return (this.currentPage - 1) * this.resolvedPageSize + 1
209
+ },
210
+
211
+ paginationEnd() {
212
+ if (!this.filteredFilesCount) return 0
213
+ return Math.min(this.currentPage * this.resolvedPageSize, this.filteredFilesCount)
214
+ },
215
+
149
216
  hasActiveSearch() {
150
217
  return this.searchTerms.length > 0
151
218
  },
@@ -163,6 +230,13 @@ export default {
163
230
  return this.filteredFiles.slice(0, this.effectiveVisibleCount)
164
231
  },
165
232
 
233
+ paginatedFiles() {
234
+ if (!this.paginate) return this.visibleFiles
235
+
236
+ const startIndex = (this.currentPage - 1) * this.resolvedPageSize
237
+ return this.filteredFiles.slice(startIndex, startIndex + this.resolvedPageSize)
238
+ },
239
+
166
240
  canShowMore() {
167
241
  return (
168
242
  !!this.initialVisibleCount &&
@@ -220,6 +294,7 @@ export default {
220
294
 
221
295
  shouldShowFooter() {
222
296
  return (
297
+ !this.paginate &&
223
298
  this.totalFilesCount > 0 &&
224
299
  (this.canShowMore || this.hasActiveSearch || this.hasLargeResultSet)
225
300
  )
@@ -241,6 +316,34 @@ export default {
241
316
  return 'No files available.'
242
317
  },
243
318
 
319
+ showTopPagination() {
320
+ return this.paginate || this.hasActiveSearch || !!this.initialVisibleCount
321
+ },
322
+
323
+ paginationSummary() {
324
+ if (!this.filteredFilesCount) {
325
+ return this.hasActiveSearch ? 'Showing 0 matching files.' : 'Showing 0 files.'
326
+ }
327
+
328
+ if (this.paginate) {
329
+ if (this.hasActiveSearch) {
330
+ return `Showing ${this.paginationStart}-${this.paginationEnd} of ${this.filteredFilesCount} matching files.`
331
+ }
332
+
333
+ return `Showing ${this.paginationStart}-${this.paginationEnd} of ${this.filteredFilesCount} files.`
334
+ }
335
+
336
+ if (this.canShowMore) {
337
+ return `Showing ${this.visibleFiles.length} of ${this.filteredFilesCount} files.`
338
+ }
339
+
340
+ if (this.hasActiveSearch) {
341
+ return `Showing ${this.filteredFilesCount} matching files.`
342
+ }
343
+
344
+ return `Showing ${this.filteredFilesCount} files.`
345
+ },
346
+
244
347
  categorizedFileItems() {
245
348
  if (this.groupByDate) {
246
349
  return this.fileItemsGroupedByDate
@@ -255,7 +358,7 @@ export default {
255
358
  },
256
359
 
257
360
  fileItemsGroupedByCategory() {
258
- return this.visibleFiles
361
+ return this.paginatedFiles
259
362
  .reduce((a, c) => {
260
363
  let categories =
261
364
  c.categories && c.categories.length > 0
@@ -271,7 +374,7 @@ export default {
271
374
  return {
272
375
  name: c,
273
376
  items: mapFilesToListItems(
274
- this.visibleFiles
377
+ this.paginatedFiles
275
378
  .map(f => ({
276
379
  ...f,
277
380
  categories:
@@ -296,7 +399,7 @@ export default {
296
399
  },
297
400
 
298
401
  fileItemsGroupedByCategoryDescending() {
299
- return this.visibleFiles
402
+ return this.paginatedFiles
300
403
  .reduce((a, c) => {
301
404
  let categories =
302
405
  c.categories && c.categories.length > 0
@@ -312,7 +415,7 @@ export default {
312
415
  return {
313
416
  name: c,
314
417
  items: mapFilesToListItems(
315
- this.visibleFiles
418
+ this.paginatedFiles
316
419
  .map(f => ({
317
420
  ...f,
318
421
  categories:
@@ -337,7 +440,7 @@ export default {
337
440
  },
338
441
 
339
442
  fileItemsGroupedByTag() {
340
- return this.visibleFiles
443
+ return this.paginatedFiles
341
444
  .reduce((a, t) => {
342
445
  let tags = t.tags && t.tags.length > 0 ? t.tags : ['Untagged']
343
446
  for (const tag of tags) {
@@ -350,7 +453,7 @@ export default {
350
453
  return {
351
454
  name: t,
352
455
  items: mapFilesToListItems(
353
- this.visibleFiles
456
+ this.paginatedFiles
354
457
  .map(f => ({
355
458
  ...f,
356
459
  tags: f.tags && f.tags.length > 0 ? f.tags : ['Untagged'],
@@ -372,7 +475,7 @@ export default {
372
475
  },
373
476
 
374
477
  fileItemsGroupedByDate() {
375
- return this.visibleFiles
478
+ return this.paginatedFiles
376
479
  .reduce((a, c) => {
377
480
  let date = new Date(c.lastModifiedDateTime).toLocaleDateString()
378
481
  if (a.indexOf(date) === -1) a.push(date)
@@ -383,7 +486,7 @@ export default {
383
486
  name: d,
384
487
  unix: new Date(d).valueOf(),
385
488
  items: mapFilesToListItems(
386
- this.visibleFiles.filter(
489
+ this.paginatedFiles.filter(
387
490
  f => new Date(f.lastModifiedDateTime).toLocaleDateString() === d
388
491
  )
389
492
  )
@@ -405,7 +508,7 @@ export default {
405
508
  fileItemsGroupedByOwner() {
406
509
  return [
407
510
  {
408
- items: mapFilesToListItems(this.visibleFiles).map(t => ({
511
+ items: mapFilesToListItems(this.paginatedFiles).map(t => ({
409
512
  ...t,
410
513
  bordered: false,
411
514
  alignTop: false,
@@ -420,6 +523,7 @@ export default {
420
523
  watch: {
421
524
  files: {
422
525
  handler() {
526
+ this.currentPage = 1
423
527
  this.resetVisibleCount()
424
528
  this.$nextTick(() => {
425
529
  // console.log('Change in Files detected:', this.files)
@@ -431,16 +535,24 @@ export default {
431
535
  immediate: true,
432
536
  },
433
537
  searchQuery() {
538
+ this.currentPage = 1
434
539
  this.resetVisibleCount()
435
540
  },
436
541
  },
437
542
 
438
543
  mounted() {
544
+ this.currentPage = 1
439
545
  this.resetVisibleCount()
440
546
  if (this.url) this.loadFiles()
441
547
  },
442
548
 
443
549
  methods: {
550
+ nextPage() {
551
+ this.currentPage = Math.min(this.currentPage + 1, this.totalPages)
552
+ },
553
+ prevPage() {
554
+ this.currentPage = Math.max(this.currentPage - 1, 1)
555
+ },
444
556
  resetVisibleCount() {
445
557
  this.visibleCount = this.initialVisibleCount > 0
446
558
  ? this.initialVisibleCount
@@ -503,7 +615,7 @@ export default {
503
615
  .asd20-file-list__toolbar {
504
616
  display: flex;
505
617
  align-items: center;
506
- gap: space(0.25);
618
+ justify-content: flex-end;
507
619
  }
508
620
 
509
621
  .asd20-file-list__search {
@@ -521,10 +633,6 @@ export default {
521
633
  font-size: 1rem;
522
634
  min-height: 40px;
523
635
  }
524
-
525
- :deep(.asd20-search-field__extra) {
526
- padding: 0 space(0.5) 0 space(0.75);
527
- }
528
636
  }
529
637
 
530
638
  .asd20-file-list__category-group {
@@ -537,6 +645,29 @@ export default {
537
645
  }
538
646
  }
539
647
 
648
+ .asd20-file-list__pagination {
649
+ display: flex;
650
+ align-items: center;
651
+ justify-content: space-between;
652
+ flex-wrap: wrap;
653
+ margin: 0;
654
+ }
655
+
656
+ .asd20-file-list__pagination-summary {
657
+ margin: 0;
658
+ }
659
+
660
+ .asd20-file-list__pagination-actions {
661
+ display: flex;
662
+ flex-wrap: wrap;
663
+ align-items: center;
664
+ gap: space(0.5);
665
+ }
666
+
667
+ .asd20-file-list__pagination-page {
668
+ margin: 0;
669
+ }
670
+
540
671
  .asd20-file-list__empty {
541
672
  width: 100%;
542
673
  padding: space(0.75) 0;
@@ -547,7 +678,7 @@ export default {
547
678
  display: flex;
548
679
  flex-wrap: wrap;
549
680
  align-items: center;
550
- justify-content: space-between;
681
+ justify-content: center;
551
682
  gap: space(0.5);
552
683
  padding-top: space(0.75);
553
684
  }
@@ -563,12 +694,15 @@ export default {
563
694
  gap: space(0.5);
564
695
  }
565
696
 
697
+
566
698
  @media (max-width: 767px) {
567
699
  .asd20-file-list__toolbar {
568
700
  display: block;
569
- flex-wrap: wrap;
570
701
  width: 100%;
571
702
  }
703
+ .asd20-file-list__pagination {
704
+ justify-content: center;
705
+ }
572
706
 
573
707
  .asd20-file-list__search {
574
708
  min-width: 100%;
@@ -577,12 +711,15 @@ export default {
577
711
  }
578
712
 
579
713
  @media (min-width: 1024px) {
580
- .asd20-file-list__search {
581
- margin: space(0.5) space(0.5);
582
-
583
- :deep(.asd20-icon) {
584
- margin-left: 0;
585
- }
714
+ // .asd20-file-list__search {
715
+ // margin: space(0.5) space(0.5);
716
+
717
+ // :deep(.asd20-icon) {
718
+ // margin-left: 0;
719
+ // }
720
+ // }
721
+ .asd20-file-list__pagination {
722
+ justify-content: space-between;
586
723
  }
587
724
  }
588
725
  </style>
@@ -25,6 +25,12 @@
25
25
  </div>
26
26
  <slot name="header" />
27
27
  </div>
28
+ <div
29
+ v-if="$slots['after-header']"
30
+ class="asd20-list__after-header"
31
+ >
32
+ <slot name="after-header" />
33
+ </div>
28
34
  <asd20-viewport
29
35
  scrollable
30
36
  :max-height="maxHeight"
@@ -151,6 +157,10 @@ export default {
151
157
  color: var(--color__primary);
152
158
  }
153
159
 
160
+ .asd20-list__after-header {
161
+ margin-bottom: space(0.5);
162
+ }
163
+
154
164
  .asd20-list--multi-column {
155
165
  .asd20-list__items {
156
166
  list-style: none;
@@ -25,6 +25,12 @@
25
25
  </div>
26
26
  <slot name="header" />
27
27
  </div>
28
+ <div
29
+ v-if="$slots['after-header']"
30
+ class="asd20-list__after-header"
31
+ >
32
+ <slot name="after-header" />
33
+ </div>
28
34
  <asd20-viewport
29
35
  ref="viewport"
30
36
  scrollable
@@ -156,6 +162,10 @@ export default {
156
162
  color: var(--color__primary);
157
163
  }
158
164
 
165
+ .asd20-list__after-header {
166
+ margin-bottom: space(0.5);
167
+ }
168
+
159
169
  .asd20-list--multi-column {
160
170
  .asd20-list__items {
161
171
  list-style: none;
@@ -80,8 +80,8 @@
80
80
  v-bind="filesFeedProps"
81
81
  :multi-column="true"
82
82
  searchable
83
- :initial-visible-count="50"
84
- :visible-count-step="50"
83
+ paginate
84
+ :page-size="50"
85
85
  search-placeholder="Search files"
86
86
  max-height="auto"
87
87
  @files-in-view="$emit('files-in-view')"
@@ -22,6 +22,33 @@ function hasIncomingProp(vm, propName) {
22
22
  )
23
23
  }
24
24
 
25
+ function resolveStore(vm) {
26
+ if (!vm) return null
27
+
28
+ const internal = vm.$
29
+ const context = internal && internal.ctx
30
+ if (context && context.$store) {
31
+ return context.$store
32
+ }
33
+
34
+ if (vm.$options && vm.$options.store) {
35
+ return vm.$options.store
36
+ }
37
+
38
+ const parentContext = internal && internal.parent && internal.parent.ctx
39
+ if (parentContext && parentContext.$store) {
40
+ return parentContext.$store
41
+ }
42
+
43
+ return (
44
+ internal &&
45
+ internal.appContext &&
46
+ internal.appContext.config &&
47
+ internal.appContext.config.globalProperties &&
48
+ internal.appContext.config.globalProperties.$store
49
+ )
50
+ }
51
+
25
52
  export default function(variableName, propOptions, storeModuleName) {
26
53
  const pascalCaseName = variableName[0].toUpperCase() + variableName.slice(1)
27
54
  const resolvedName = `resolved${pascalCaseName}`
@@ -41,14 +68,15 @@ export default function(variableName, propOptions, storeModuleName) {
41
68
  return this[variableName]
42
69
  }
43
70
 
44
- try {
45
- const storeValue = storeModuleName
46
- ? this.$store['state'][storeModuleName][variableName]
47
- : this.$store.state[variableName]
48
- return storeValue !== undefined ? storeValue : this[localStateName]
49
- } catch {
50
- return this[localStateName]
51
- }
71
+ const store = resolveStore(this)
72
+ const state = store && store.state
73
+ const storeState = storeModuleName && state ? state[storeModuleName] : state
74
+ const storeValue =
75
+ storeState && Object.prototype.hasOwnProperty.call(storeState, variableName)
76
+ ? storeState[variableName]
77
+ : undefined
78
+
79
+ return storeValue !== undefined ? storeValue : this[localStateName]
52
80
  },
53
81
  // emit an update event and value and call method to set value in store.
54
82
  set(value) {
@@ -68,9 +96,10 @@ export default function(variableName, propOptions, storeModuleName) {
68
96
  },
69
97
  }
70
98
  mixin.methods[`set${pascalCaseName}`] = function(value) {
71
- if (!this.$store || typeof this.$store.dispatch !== 'function') return false
99
+ const store = resolveStore(this)
100
+ if (!store || typeof store.dispatch !== 'function') return false
72
101
 
73
- this.$store.dispatch(
102
+ store.dispatch(
74
103
  `${storeModuleName ? storeModuleName + '/' : ''}set${pascalCaseName}`,
75
104
  value
76
105
  )