Skip to content

Commit 9a88a5a

Browse files
committed
Update code with use of reusables, fix some checks
1 parent cda6585 commit 9a88a5a

6 files changed

Lines changed: 308 additions & 385 deletions

File tree

web-app/packages/admin-lib/src/modules/admin/components/AccountsTable.vue

Lines changed: 62 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<PInputText
2525
placeholder="Search accounts"
2626
data-cy="search-members-field"
27-
v-model="searchByName"
27+
v-model="search"
2828
class="w-full"
2929
@input="onSearch"
3030
/>
@@ -44,49 +44,45 @@
4444
:first="(options.page - 1) * options.itemsPerPage"
4545
:sort-field="options.sortBy[0]"
4646
:sort-order="options.sortDesc[0] ? -1 : 1"
47+
:rowHover="true"
4748
removableSort
4849
reorderable-columns
4950
@page="onPage"
50-
@row-click="rowClick"
5151
@sort="onSort"
5252
data-cy="accounts-table"
5353
>
54-
<template v-for="header in headers" :key="header.field">
55-
<PColumn
56-
v-if="header.field === 'username'"
57-
:field="header.field"
58-
:header="header.header"
59-
:sortable="header.sortable"
60-
>
61-
<template #body="slotProps">
62-
<router-link
63-
class="title-t4"
64-
:to="{
65-
name: 'account',
66-
params: { username: slotProps.data.username }
67-
}"
68-
>
69-
{{ slotProps.data.username }}
70-
</router-link>
71-
</template>
72-
</PColumn>
73-
<PColumn
74-
v-else-if="header.field === 'active'"
75-
:header="header.header"
76-
:field="header.field"
77-
>
78-
<template #body="slotProps">
79-
<i v-if="slotProps.data.active" class="ti ti-check" />
54+
<PColumn field="username" header="Username" :sortable="true">
55+
<template #body="{ data }">
56+
<router-link
57+
:to="accountRoute(data)"
58+
class="dt-row-link title-t4"
59+
>
60+
{{ data.username }}
61+
</router-link>
62+
</template>
63+
</PColumn>
64+
<PColumn field="email" header="Email" :sortable="true">
65+
<template #body="{ data }">
66+
<router-link :to="accountRoute(data)" class="dt-row-link">
67+
{{ data.email }}
68+
</router-link>
69+
</template>
70+
</PColumn>
71+
<PColumn field="profile.name" header="Full name">
72+
<template #body="{ data }">
73+
<router-link :to="accountRoute(data)" class="dt-row-link">
74+
{{ data.profile?.name }}
75+
</router-link>
76+
</template>
77+
</PColumn>
78+
<PColumn field="active" header="Active">
79+
<template #body="{ data }">
80+
<router-link :to="accountRoute(data)" class="dt-row-link">
81+
<i v-if="data.active" class="ti ti-check" />
8082
<i v-else class="ti ti-x" />
81-
</template>
82-
</PColumn>
83-
<PColumn
84-
v-else
85-
:field="header.field"
86-
:header="header.header"
87-
:sortable="header.sortable"
88-
></PColumn>
89-
</template>
83+
</router-link>
84+
</template>
85+
</PColumn>
9086
<template #paginatorstart>
9187
<PButton
9288
icon="ti ti-refresh"
@@ -106,18 +102,12 @@
106102
<script lang="ts">
107103
import {
108104
PaginatedUsersParams,
105+
useDataTableSearch,
109106
useDialogStore,
110-
TableDataHeader,
111107
AppContainer,
112108
AppSection
113109
} from '@mergin/lib'
114-
import debounce from 'lodash/debounce'
115-
import { mapActions, mapState } from 'pinia'
116-
import {
117-
DataTablePageEvent,
118-
DataTableRowClickEvent,
119-
DataTableSortEvent
120-
} from 'primevue/datatable'
110+
import { mapState } from 'pinia'
121111
import { defineComponent } from 'vue'
122112
123113
import { AdminRoutes } from '@/modules'
@@ -130,134 +120,50 @@ export default defineComponent({
130120
AppContainer,
131121
AppSection
132122
},
133-
data() {
123+
setup() {
124+
const adminStore = useAdminStore()
125+
const dialogStore = useDialogStore()
126+
127+
const tableSearch = useDataTableSearch({
128+
defaultSortBy: 'username',
129+
defaultSortDesc: false
130+
})
131+
132+
tableSearch.setFetchFn((signal) => {
133+
const { options, search } = tableSearch
134+
const params: PaginatedUsersParams = {
135+
page: options.page,
136+
per_page: options.itemsPerPage
137+
}
138+
if (options.sortBy[0]) {
139+
params.descending = options.sortDesc[0]
140+
params.order_by = options.sortBy[0]
141+
}
142+
if (search.value) params.like = search.value.trim()
143+
adminStore.fetchUsers({ params, signal })
144+
})
145+
134146
return {
135-
options: {
136-
sortBy: ['username'],
137-
sortDesc: [false],
138-
itemsPerPage: 20,
139-
page: 1,
140-
perPageOptions: [20, 50, 100]
141-
},
142-
searchByName: '',
143-
headers: [
144-
{ field: 'username', header: 'Username', sortable: true },
145-
{ field: 'email', header: 'Email', sortable: true },
146-
{ field: 'profile.name', header: 'Full name' },
147-
{ field: 'active', header: 'Active' }
148-
] as TableDataHeader[],
149-
abortController: null as AbortController | null
147+
...tableSearch,
148+
show: dialogStore.show.bind(dialogStore)
150149
}
151150
},
152151
computed: {
153152
...mapState(useAdminStore, ['users', 'loading'])
154153
},
155154
created() {
156-
// Restore any search/sort/page state from the URL before the first fetch
157155
this.initFromQuery()
158-
// Delay search-triggered fetches so rapid typing doesn't spam the API
159-
this.onSearch = debounce(this.onSearch, 500)
160156
this.doFetch()
161157
},
162158
methods: {
163-
...mapActions(useAdminStore, ['fetchUsers']),
164-
...mapActions(useDialogStore, ['show']),
165-
166-
// Seed local state from URL query params so the page is shareable / survives navigation
167-
initFromQuery() {
168-
const q = this.$route.query
169-
if (q.q) this.searchByName = String(q.q)
170-
if (q.page) this.options.page = Number(q.page)
171-
if (q.per_page) this.options.itemsPerPage = Number(q.per_page)
172-
if (q.order_by) this.options.sortBy[0] = String(q.order_by)
173-
if (q.desc) this.options.sortDesc[0] = q.desc === 'true'
174-
},
175-
176-
// Reflect current search/sort/page state into the URL (defaults are omitted to keep URLs clean)
177-
updateQuery() {
178-
const query: Record<string, string> = {}
179-
if (this.searchByName) query.q = this.searchByName
180-
if (this.options.page > 1) query.page = String(this.options.page)
181-
if (this.options.itemsPerPage !== 20)
182-
query.per_page = String(this.options.itemsPerPage)
183-
if (this.options.sortBy[0] && this.options.sortBy[0] !== 'username')
184-
query.order_by = this.options.sortBy[0]
185-
if (this.options.sortDesc[0]) query.desc = 'true'
186-
// replace (not push) so back-button skips intermediate search states
187-
this.$router.replace({ query })
188-
},
189-
190-
// Single entry point for all fetches: cancels any in-flight request, syncs the URL, then fetches
191-
doFetch() {
192-
// Abort the previous request so a stale slower response can't overwrite a newer one
193-
this.abortController?.abort()
194-
this.abortController = new AbortController()
195-
this.updateQuery()
196-
this.fetchUsers({
197-
params: this.getParams(),
198-
signal: this.abortController.signal
199-
})
200-
},
201-
202-
// Called on every keystroke (debounced); resets to page 1 so results start from the beginning
203-
onSearch() {
204-
this.options.page = 1
205-
this.doFetch()
206-
},
207-
208-
getParams(): PaginatedUsersParams {
209-
const params = {
210-
page: this.options.page,
211-
per_page: this.options.itemsPerPage
212-
} as PaginatedUsersParams
213-
if (this.options.sortBy[0]) {
214-
params.descending = this.options.sortDesc[0]
215-
params.order_by = this.options.sortBy[0]
216-
}
217-
if (this.searchByName) {
218-
params.like = this.searchByName.trim()
219-
}
220-
return params
221-
},
222-
223-
onRefresh() {
224-
this.doFetch()
225-
},
226-
227-
onPage(event: DataTablePageEvent) {
228-
this.options.page = event.page + 1
229-
this.options.itemsPerPage = event.rows
230-
this.doFetch()
231-
},
232-
233-
onSort(event: DataTableSortEvent) {
234-
this.options.sortBy[0] = event.sortField?.toString()
235-
this.options.sortDesc[0] = event.sortOrder < 1
236-
this.doFetch()
237-
},
238-
239-
rowClick(event: DataTableRowClickEvent) {
240-
const originalEvent = event.originalEvent as MouseEvent
241-
// Let the browser handle clicks that originate from a link inside the row (e.g. username column)
242-
if ((originalEvent.target as HTMLElement).closest('a')) return
243-
244-
const location = {
245-
name: AdminRoutes.ACCOUNT,
246-
params: { username: event.data.username }
247-
}
248-
// Ctrl/Cmd/Shift+click opens in a new tab; plain click navigates in the same tab
249-
if (originalEvent.ctrlKey || originalEvent.metaKey || originalEvent.shiftKey) {
250-
window.open(this.$router.resolve(location).href, '_blank')
251-
} else {
252-
this.$router.push(location)
253-
}
159+
accountRoute(data) {
160+
return { name: AdminRoutes.ACCOUNT, params: { username: data.username } }
254161
},
255162
256163
createUserDialog() {
257164
const dialog = { maxWidth: 500, header: 'Create user' }
258165
const listeners = {
259166
success: () => {
260-
// After creating a user, go back to page 1 so the new account is visible
261167
this.options.page = 1
262168
this.doFetch()
263169
}

0 commit comments

Comments
 (0)