|
| 1 | +/** |
| 2 | + * Copyright (c) HashiCorp, Inc. |
| 3 | + * SPDX-License-Identifier: MPL-2.0 |
| 4 | + */ |
| 5 | +import Component from '@glimmer/component'; |
| 6 | +import { array, hash } from '@ember/helper'; |
| 7 | +import { inject as service } from '@ember/service'; |
| 8 | +import type RouterService from '@ember/routing/router-service'; |
| 9 | + |
| 10 | +import USERS from 'showcase/mocks/user-data'; |
| 11 | +import type { User } from 'showcase/mocks/user-data'; |
| 12 | + |
| 13 | +import { |
| 14 | + HdsPaginationCompact, |
| 15 | + HdsTable, |
| 16 | +} from '@hashicorp/design-system-components/components'; |
| 17 | +import type { HdsPaginationDirections } from '@hashicorp/design-system-components/components/hds/pagination/types'; |
| 18 | + |
| 19 | +const getCursorParts = (cursor: string | null, records: User[]) => { |
| 20 | + if (!cursor) { |
| 21 | + return { direction: 'next', cursorID: null, cursorIndex: -1 }; |
| 22 | + } |
| 23 | + |
| 24 | + const token = atob(cursor); |
| 25 | + const tokenParts = [...token.split('__')]; |
| 26 | + const direction = tokenParts[0]; |
| 27 | + const cursorID = tokenParts[1] ? parseInt(tokenParts[1]) : undefined; |
| 28 | + const cursorIndex = records.findIndex((element) => element.id === cursorID); |
| 29 | + |
| 30 | + return { direction, cursorID, cursorIndex }; |
| 31 | +}; |
| 32 | + |
| 33 | +const getNewPrevNextCursors = ( |
| 34 | + cursor: string | null, |
| 35 | + pageSize: number, |
| 36 | + records: User[], |
| 37 | +) => { |
| 38 | + const { direction, cursorIndex } = getCursorParts(cursor, records); |
| 39 | + |
| 40 | + let newPrevCursor; |
| 41 | + let newNextCursor; |
| 42 | + |
| 43 | + const prevCursorIndex = |
| 44 | + direction === 'prev' ? cursorIndex - pageSize : cursorIndex; |
| 45 | + if (prevCursorIndex > 0) { |
| 46 | + const newPrevRecordId = records[prevCursorIndex]?.id; |
| 47 | + newPrevCursor = btoa(`prev__${newPrevRecordId}`); |
| 48 | + } else { |
| 49 | + newPrevCursor = null; |
| 50 | + } |
| 51 | + |
| 52 | + const nextCursorIndex = |
| 53 | + direction === 'next' ? cursorIndex + pageSize : cursorIndex; |
| 54 | + if (nextCursorIndex < records.length) { |
| 55 | + const newNextRecordId = records[nextCursorIndex]?.id; |
| 56 | + newNextCursor = btoa(`next__${newNextRecordId}`); |
| 57 | + } else { |
| 58 | + newNextCursor = null; |
| 59 | + } |
| 60 | + |
| 61 | + return { |
| 62 | + newPrevCursor, |
| 63 | + newNextCursor, |
| 64 | + }; |
| 65 | +}; |
| 66 | + |
| 67 | +interface CodeFragmentWithCompactAndRoutingSignature { |
| 68 | + Args: { |
| 69 | + nextCursor: string | null; |
| 70 | + prevCursor: string | null; |
| 71 | + pageSize: number; |
| 72 | + }; |
| 73 | +} |
| 74 | + |
| 75 | +export default class CodeFragmentWithCompactAndRouting extends Component<CodeFragmentWithCompactAndRoutingSignature> { |
| 76 | + @service declare router: RouterService; |
| 77 | + |
| 78 | + get paginatedData() { |
| 79 | + const { prevCursor, nextCursor, pageSize } = this.args; |
| 80 | + |
| 81 | + let token = ''; |
| 82 | + if (prevCursor) { |
| 83 | + token = prevCursor; |
| 84 | + } else if (nextCursor) { |
| 85 | + token = nextCursor; |
| 86 | + } |
| 87 | + |
| 88 | + const { direction, cursorIndex } = getCursorParts(token, USERS); |
| 89 | + |
| 90 | + let start; |
| 91 | + let end; |
| 92 | + |
| 93 | + if (direction === 'prev') { |
| 94 | + end = cursorIndex; |
| 95 | + // we want to avoid having a negative `start` index for the `array.slide` method (it happens if the cursorIndex is smaller than the selected page size) |
| 96 | + start = Math.max(0, cursorIndex - pageSize); |
| 97 | + } else { |
| 98 | + start = cursorIndex; |
| 99 | + end = cursorIndex + pageSize; |
| 100 | + } |
| 101 | + |
| 102 | + return USERS.slice(start, end); |
| 103 | + } |
| 104 | + |
| 105 | + get newCursors() { |
| 106 | + const { prevCursor, nextCursor, pageSize } = this.args; |
| 107 | + |
| 108 | + let cursor = ''; |
| 109 | + // In cloud UI they use two distinct query params for the cursor depending if it's "prev" or "next" |
| 110 | + if (prevCursor) { |
| 111 | + cursor = prevCursor; |
| 112 | + } else if (nextCursor) { |
| 113 | + cursor = nextCursor; |
| 114 | + } |
| 115 | + return getNewPrevNextCursors(cursor, pageSize, USERS); |
| 116 | + } |
| 117 | + |
| 118 | + get isPrevButtonDisabled() { |
| 119 | + const { newPrevCursor } = this.newCursors; |
| 120 | + return newPrevCursor === null; |
| 121 | + } |
| 122 | + |
| 123 | + get isNextButtonDisabled() { |
| 124 | + const { newNextCursor } = this.newCursors; |
| 125 | + return newNextCursor === null; |
| 126 | + } |
| 127 | + |
| 128 | + get demoRouteName() { |
| 129 | + // eg. 'components.pagination'; |
| 130 | + const routeName = this.router.currentRouteName; |
| 131 | + return routeName ?? ''; |
| 132 | + } |
| 133 | + |
| 134 | + get demoQueryFunction() { |
| 135 | + const { newPrevCursor, newNextCursor } = this.newCursors; |
| 136 | + const currPrevCursor = this.args.prevCursor; |
| 137 | + const currNextCursor = this.args.nextCursor; |
| 138 | + |
| 139 | + return (page: HdsPaginationDirections, pageSize?: number) => { |
| 140 | + // for the "compact" pagination when the user changes the page size and the `onPageSizeChange` function is invoked |
| 141 | + // the callback function returns a `null` value for the `page` argument so the consumer can decide how to handle the cursors acordingly |
| 142 | + |
| 143 | + if (page === null) { |
| 144 | + return { |
| 145 | + prevCursorDemoCompact: currPrevCursor, |
| 146 | + nextCursorDemoCompact: currNextCursor, |
| 147 | + pageSizeDemoCompact: pageSize, |
| 148 | + }; |
| 149 | + } else { |
| 150 | + return { |
| 151 | + prevCursorDemoCompact: page === 'prev' ? newPrevCursor : undefined, |
| 152 | + nextCursorDemoCompact: page === 'next' ? newNextCursor : undefined, |
| 153 | + pageSizeDemoCompact: pageSize, |
| 154 | + }; |
| 155 | + } |
| 156 | + }; |
| 157 | + } |
| 158 | + |
| 159 | + onPageChange = (page: HdsPaginationDirections) => { |
| 160 | + console.log('genericHandlePageChange invoked with arguments:'); |
| 161 | + console.log('page', page); |
| 162 | + console.log('pageSize', this.args.pageSize); |
| 163 | + }; |
| 164 | + |
| 165 | + onPageSizeChange = (pageSize: number) => { |
| 166 | + // there should be a better handling of how the "paginated" data list is computed and shown to the user to avoid some UX issues |
| 167 | + // for details see this thread: https://github.com/hashicorp/design-system/pull/1724#issuecomment-1768167782 |
| 168 | + this.router.transitionTo(this.demoRouteName, { |
| 169 | + queryParams: { |
| 170 | + pageSizeDemoCompact: pageSize, |
| 171 | + }, |
| 172 | + }); |
| 173 | + }; |
| 174 | + |
| 175 | + <template> |
| 176 | + <div class="shw-component-pagination-table-demo"> |
| 177 | + <HdsTable |
| 178 | + @model={{this.paginatedData}} |
| 179 | + @columns={{array |
| 180 | + (hash key="id" label="ID") |
| 181 | + (hash key="name" label="Name") |
| 182 | + (hash key="email" label="Email") |
| 183 | + (hash key="role" label="Role") |
| 184 | + }} |
| 185 | + > |
| 186 | + <:body as |B|> |
| 187 | + <B.Tr> |
| 188 | + <B.Td>{{B.data.id}}</B.Td> |
| 189 | + <B.Td>{{B.data.name}}</B.Td> |
| 190 | + <B.Td>{{B.data.email}}</B.Td> |
| 191 | + <B.Td>{{B.data.role}}</B.Td> |
| 192 | + </B.Tr> |
| 193 | + </:body> |
| 194 | + </HdsTable> |
| 195 | + <HdsPaginationCompact |
| 196 | + @queryFunction={{this.demoQueryFunction}} |
| 197 | + @showSizeSelector={{true}} |
| 198 | + @route={{this.demoRouteName}} |
| 199 | + @currentPageSize={{@pageSize}} |
| 200 | + @pageSizes={{array 5 10 30}} |
| 201 | + @isDisabledPrev={{this.isPrevButtonDisabled}} |
| 202 | + @isDisabledNext={{this.isNextButtonDisabled}} |
| 203 | + @onPageSizeChange={{this.onPageSizeChange}} |
| 204 | + @onPageChange={{this.onPageChange}} |
| 205 | + /> |
| 206 | + </div> |
| 207 | + </template> |
| 208 | +} |
0 commit comments