mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-03 17:00:53 +00:00
ENH - migrate Companies to Sky UI
This commit is contained in:
@@ -5,10 +5,12 @@ import { useCompaniesStore } from '@/stores/companies'
|
||||
import type {
|
||||
Company,
|
||||
CompanyDirectoryFilters,
|
||||
CompanyDirectoryPage,
|
||||
CompanyRequest,
|
||||
CompanyRequestPage,
|
||||
CompanySummary,
|
||||
} from '@/types/companies'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
import { nuiCall, type NuiResponse } from '@/utils/nui'
|
||||
|
||||
vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() }))
|
||||
|
||||
@@ -84,6 +86,17 @@ const filters: CompanyDirectoryFilters = {
|
||||
sort: 'relevance',
|
||||
}
|
||||
|
||||
function deferred<T>(): {
|
||||
promise: Promise<T>
|
||||
resolve: (value: T) => void
|
||||
} {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('companies store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
@@ -132,6 +145,208 @@ describe('companies store', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores an older directory response after the filters change', async () => {
|
||||
const firstResponse = deferred<NuiResponse<CompanyDirectoryPage>>()
|
||||
const secondResponse = deferred<NuiResponse<CompanyDirectoryPage>>()
|
||||
mockNuiCall
|
||||
.mockReturnValueOnce(firstResponse.promise)
|
||||
.mockReturnValueOnce(secondResponse.promise)
|
||||
const store = useCompaniesStore()
|
||||
const searchedFilters = { ...filters, search: 'medical' }
|
||||
const medical = {
|
||||
...summary,
|
||||
id: 'medical',
|
||||
name: 'Los Santos Medical',
|
||||
}
|
||||
|
||||
const firstLoad = store.loadCompanies(filters)
|
||||
const secondLoad = store.loadCompanies(searchedFilters)
|
||||
secondResponse.resolve({
|
||||
data: { categories: [], companies: [medical], nextCursor: null },
|
||||
success: true,
|
||||
})
|
||||
|
||||
await expect(secondLoad).resolves.toBe(true)
|
||||
firstResponse.resolve({
|
||||
data: { categories: [], companies: [summary], nextCursor: null },
|
||||
success: true,
|
||||
})
|
||||
|
||||
await expect(firstLoad).resolves.toBe(false)
|
||||
expect(store.directory).toEqual([medical])
|
||||
expect(store.directoryFilters).toEqual(searchedFilters)
|
||||
expect(store.directoryLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('ignores an older customer request response after the list changes', async () => {
|
||||
const firstResponse = deferred<NuiResponse<CompanyRequestPage>>()
|
||||
const secondResponse = deferred<NuiResponse<CompanyRequestPage>>()
|
||||
mockNuiCall
|
||||
.mockReturnValueOnce(firstResponse.promise)
|
||||
.mockReturnValueOnce(secondResponse.promise)
|
||||
const store = useCompaniesStore()
|
||||
const closedRequest = { ...request, id: 'request-closed' }
|
||||
|
||||
const firstLoad = store.loadMyRequests('open')
|
||||
const secondLoad = store.loadMyRequests('closed')
|
||||
secondResponse.resolve({
|
||||
data: { nextCursor: null, requests: [closedRequest], unreadCount: 1 },
|
||||
success: true,
|
||||
})
|
||||
|
||||
await expect(secondLoad).resolves.toBe(true)
|
||||
firstResponse.resolve({
|
||||
data: { nextCursor: null, requests: [request], unreadCount: 7 },
|
||||
success: true,
|
||||
})
|
||||
|
||||
await expect(firstLoad).resolves.toBe(false)
|
||||
expect(store.myRequests).toEqual([closedRequest])
|
||||
expect(store.myRequestsList).toBe('closed')
|
||||
expect(store.customerUnreadCount).toBe(1)
|
||||
expect(store.myRequestsLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps directory data and its initial error when an append fails', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({
|
||||
error: 'temporarily_unavailable',
|
||||
success: false,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
categories: [],
|
||||
companies: [
|
||||
{ ...summary, id: 'medical', name: 'Los Santos Medical' },
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
success: true,
|
||||
})
|
||||
const store = useCompaniesStore()
|
||||
store.directory = [summary]
|
||||
store.directoryError = 'initial_error'
|
||||
store.directoryFilters = { ...filters }
|
||||
store.directoryNextCursor = 'page-2'
|
||||
|
||||
expect(await store.loadCompanies(filters, true)).toBe(false)
|
||||
expect(store.directory).toEqual([summary])
|
||||
expect(store.directoryNextCursor).toBe('page-2')
|
||||
expect(store.directoryError).toBe('initial_error')
|
||||
expect(store.directoryAppendError).toBe('temporarily_unavailable')
|
||||
|
||||
expect(await store.loadCompanies(filters, true)).toBe(true)
|
||||
expect(store.directory.map((item) => item.id)).toEqual([
|
||||
'police',
|
||||
'medical',
|
||||
])
|
||||
expect(store.directoryError).toBe('initial_error')
|
||||
expect(store.directoryAppendError).toBe('')
|
||||
})
|
||||
|
||||
it('keeps customer requests and their initial error when an append fails', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({
|
||||
error: 'temporarily_unavailable',
|
||||
success: false,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
nextCursor: null,
|
||||
requests: [{ ...request, id: 'request-2' }],
|
||||
unreadCount: 2,
|
||||
},
|
||||
success: true,
|
||||
})
|
||||
const store = useCompaniesStore()
|
||||
store.customerUnreadCount = 1
|
||||
store.myRequests = [request]
|
||||
store.myRequestsError = 'initial_error'
|
||||
store.myRequestsList = 'open'
|
||||
store.myRequestsNextCursor = 'page-2'
|
||||
|
||||
expect(await store.loadMyRequests('open', true)).toBe(false)
|
||||
expect(store.myRequests).toEqual([request])
|
||||
expect(store.myRequestsNextCursor).toBe('page-2')
|
||||
expect(store.customerUnreadCount).toBe(1)
|
||||
expect(store.myRequestsError).toBe('initial_error')
|
||||
expect(store.myRequestsAppendError).toBe('temporarily_unavailable')
|
||||
|
||||
expect(await store.loadMyRequests('open', true)).toBe(true)
|
||||
expect(store.myRequests.map((item) => item.id)).toEqual([
|
||||
'request-1',
|
||||
'request-2',
|
||||
])
|
||||
expect(store.customerUnreadCount).toBe(2)
|
||||
expect(store.myRequestsError).toBe('initial_error')
|
||||
expect(store.myRequestsAppendError).toBe('')
|
||||
})
|
||||
|
||||
it('invalidates an in-flight customer page when the device scope is cleared', async () => {
|
||||
const response = deferred<NuiResponse<CompanyRequestPage>>()
|
||||
mockNuiCall.mockReturnValueOnce(response.promise)
|
||||
const store = useCompaniesStore()
|
||||
store.bindDeviceScope('device-a', 'sim-a')
|
||||
store.myRequests = [request]
|
||||
store.myRequestsList = 'open'
|
||||
store.myRequestsNextCursor = 'page-2'
|
||||
|
||||
const load = store.loadMyRequests('open', true)
|
||||
store.resetDeviceScope()
|
||||
response.resolve({
|
||||
data: {
|
||||
nextCursor: null,
|
||||
requests: [{ ...request, id: 'request-stale' }],
|
||||
unreadCount: 8,
|
||||
},
|
||||
success: true,
|
||||
})
|
||||
|
||||
await expect(load).resolves.toBe(false)
|
||||
expect(store.myRequests).toEqual([])
|
||||
expect(store.customerUnreadCount).toBe(0)
|
||||
expect(store.myRequestsLoaded).toBe(false)
|
||||
expect(store.myRequestsLoadingMore).toBe(false)
|
||||
})
|
||||
|
||||
it('guards directory appends by loading state, cursor and exact filters', async () => {
|
||||
const store = useCompaniesStore()
|
||||
store.directoryFilters = { ...filters }
|
||||
store.directoryNextCursor = 'page-2'
|
||||
store.directoryLoading = true
|
||||
|
||||
expect(await store.loadCompanies(filters, true)).toBe(false)
|
||||
store.directoryLoading = false
|
||||
expect(
|
||||
await store.loadCompanies({ ...filters, categoryId: 'public' }, true),
|
||||
).toBe(false)
|
||||
store.directoryNextCursor = null
|
||||
expect(await store.loadCompanies(filters, true)).toBe(false)
|
||||
store.directoryNextCursor = 'page-2'
|
||||
store.directoryLoadingMore = true
|
||||
expect(await store.loadCompanies(filters, true)).toBe(false)
|
||||
|
||||
expect(mockNuiCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('guards customer appends by loading state, cursor and exact list', async () => {
|
||||
const store = useCompaniesStore()
|
||||
store.myRequestsList = 'open'
|
||||
store.myRequestsNextCursor = 'page-2'
|
||||
store.myRequestsLoading = true
|
||||
|
||||
expect(await store.loadMyRequests('open', true)).toBe(false)
|
||||
store.myRequestsLoading = false
|
||||
expect(await store.loadMyRequests('closed', true)).toBe(false)
|
||||
store.myRequestsNextCursor = null
|
||||
expect(await store.loadMyRequests('open', true)).toBe(false)
|
||||
store.myRequestsNextCursor = 'page-2'
|
||||
store.myRequestsLoadingMore = true
|
||||
expect(await store.loadMyRequests('open', true)).toBe(false)
|
||||
|
||||
expect(mockNuiCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses server-provided unread counts for the app badge', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: {
|
||||
|
||||
@@ -35,6 +35,20 @@ function mergeById<T extends { id: string }>(current: T[], incoming: T[]): T[] {
|
||||
return [...merged.values()]
|
||||
}
|
||||
|
||||
function sameDirectoryFilters(
|
||||
current: CompanyDirectoryFilters,
|
||||
requested: CompanyDirectoryFilters,
|
||||
): boolean {
|
||||
return (
|
||||
current.acceptsRequests === requested.acceptsRequests &&
|
||||
current.availability === requested.availability &&
|
||||
current.categoryId === requested.categoryId &&
|
||||
current.hasLocation === requested.hasLocation &&
|
||||
current.search === requested.search &&
|
||||
current.sort === requested.sort
|
||||
)
|
||||
}
|
||||
|
||||
export const useCompaniesStore = defineStore('companies', {
|
||||
state: () => ({
|
||||
categories: [] as CompanyDirectoryPage['categories'],
|
||||
@@ -49,11 +63,13 @@ export const useCompaniesStore = defineStore('companies', {
|
||||
search: '',
|
||||
sort: 'relevance',
|
||||
} as CompanyDirectoryFilters,
|
||||
directoryAppendError: '',
|
||||
directoryError: '',
|
||||
directoryLoaded: false,
|
||||
directoryLoading: false,
|
||||
directoryLoadingMore: false,
|
||||
directoryNextCursor: null as string | null,
|
||||
directoryRequestGeneration: 0,
|
||||
deviceScopeKey: '',
|
||||
deviceScopeVersion: 0,
|
||||
members: [] as CompanyMember[],
|
||||
@@ -61,12 +77,14 @@ export const useCompaniesStore = defineStore('companies', {
|
||||
mutationError: '',
|
||||
mutating: false,
|
||||
myRequests: [] as CompanyRequestSummary[],
|
||||
myRequestsAppendError: '',
|
||||
myRequestsList: 'open' as CompanyRequestList,
|
||||
myRequestsError: '',
|
||||
myRequestsLoaded: false,
|
||||
myRequestsLoading: false,
|
||||
myRequestsLoadingMore: false,
|
||||
myRequestsNextCursor: null as string | null,
|
||||
myRequestsRequestGeneration: 0,
|
||||
request: null as CompanyRequest | null,
|
||||
requestError: '',
|
||||
requestLoading: false,
|
||||
@@ -151,38 +169,67 @@ export const useCompaniesStore = defineStore('companies', {
|
||||
filters: CompanyDirectoryFilters,
|
||||
append = false,
|
||||
): Promise<boolean> {
|
||||
if (append && (!this.directoryNextCursor || this.directoryLoadingMore)) {
|
||||
const requestFilters = { ...filters }
|
||||
if (
|
||||
append &&
|
||||
(this.directoryLoading ||
|
||||
this.directoryLoadingMore ||
|
||||
!this.directoryNextCursor ||
|
||||
!sameDirectoryFilters(this.directoryFilters, requestFilters))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (append) this.directoryLoadingMore = true
|
||||
else this.directoryLoading = true
|
||||
if (append) {
|
||||
this.directoryLoadingMore = true
|
||||
} else {
|
||||
this.directoryRequestGeneration += 1
|
||||
this.directoryLoading = true
|
||||
this.directoryLoadingMore = false
|
||||
this.directoryAppendError = ''
|
||||
this.directoryError = ''
|
||||
this.directoryFilters = requestFilters
|
||||
}
|
||||
this.directoryLoaded = true
|
||||
this.directoryFilters = { ...filters }
|
||||
const requestGeneration = this.directoryRequestGeneration
|
||||
const requestCursor = append ? this.directoryNextCursor : null
|
||||
const response = await nuiCall<CompanyDirectoryPage>('companies:list', {
|
||||
acceptsRequests: filters.acceptsRequests,
|
||||
availability: filters.availability,
|
||||
categoryId: filters.categoryId,
|
||||
cursor: append ? this.directoryNextCursor : null,
|
||||
hasLocation: filters.hasLocation,
|
||||
search: filters.search,
|
||||
sort: filters.sort,
|
||||
acceptsRequests: requestFilters.acceptsRequests,
|
||||
availability: requestFilters.availability,
|
||||
categoryId: requestFilters.categoryId,
|
||||
cursor: requestCursor,
|
||||
hasLocation: requestFilters.hasLocation,
|
||||
search: requestFilters.search,
|
||||
sort: requestFilters.sort,
|
||||
})
|
||||
this.directoryLoading = false
|
||||
this.directoryLoadingMore = false
|
||||
if (!response.success || !response.data) {
|
||||
this.directoryError = response.error ?? 'request_failed'
|
||||
if (!append) {
|
||||
this.directory = []
|
||||
this.directoryNextCursor = null
|
||||
const isCurrentRequest =
|
||||
requestGeneration === this.directoryRequestGeneration &&
|
||||
sameDirectoryFilters(this.directoryFilters, requestFilters)
|
||||
if (!isCurrentRequest) {
|
||||
if (requestGeneration === this.directoryRequestGeneration) {
|
||||
if (append) this.directoryLoadingMore = false
|
||||
else this.directoryLoading = false
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (append) this.directoryLoadingMore = false
|
||||
else this.directoryLoading = false
|
||||
if (!response.success || !response.data) {
|
||||
if (append) {
|
||||
this.directoryAppendError = response.error ?? 'request_failed'
|
||||
return false
|
||||
}
|
||||
this.directoryError = response.error ?? 'request_failed'
|
||||
this.directory = []
|
||||
this.directoryNextCursor = null
|
||||
return false
|
||||
}
|
||||
this.categories = response.data.categories ?? this.categories
|
||||
this.directory = append
|
||||
? mergeById(this.directory, response.data.companies)
|
||||
: response.data.companies
|
||||
this.directoryNextCursor = response.data.nextCursor
|
||||
this.directoryError = ''
|
||||
if (append) this.directoryAppendError = ''
|
||||
else this.directoryError = ''
|
||||
return true
|
||||
},
|
||||
async loadCompany(companyId: string): Promise<boolean> {
|
||||
@@ -206,37 +253,64 @@ export const useCompaniesStore = defineStore('companies', {
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
append &&
|
||||
(!this.myRequestsNextCursor || this.myRequestsLoadingMore)
|
||||
(this.myRequestsLoading ||
|
||||
this.myRequestsLoadingMore ||
|
||||
!this.myRequestsNextCursor ||
|
||||
list !== this.myRequestsList)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (append) this.myRequestsLoadingMore = true
|
||||
else this.myRequestsLoading = true
|
||||
if (append) {
|
||||
this.myRequestsLoadingMore = true
|
||||
} else {
|
||||
this.myRequestsRequestGeneration += 1
|
||||
this.myRequestsLoading = true
|
||||
this.myRequestsLoadingMore = false
|
||||
this.myRequestsAppendError = ''
|
||||
this.myRequestsError = ''
|
||||
this.myRequestsList = list
|
||||
}
|
||||
this.myRequestsLoaded = true
|
||||
this.myRequestsList = list
|
||||
const deviceScopeVersion = this.deviceScopeVersion
|
||||
const requestGeneration = this.myRequestsRequestGeneration
|
||||
const requestCursor = append ? this.myRequestsNextCursor : null
|
||||
const response = await nuiCall<CompanyRequestPage>(
|
||||
'companies:my-requests',
|
||||
{
|
||||
cursor: append ? this.myRequestsNextCursor : null,
|
||||
cursor: requestCursor,
|
||||
list,
|
||||
},
|
||||
)
|
||||
this.myRequestsLoading = false
|
||||
this.myRequestsLoadingMore = false
|
||||
if (deviceScopeVersion !== this.deviceScopeVersion) return false
|
||||
const isCurrentRequest =
|
||||
deviceScopeVersion === this.deviceScopeVersion &&
|
||||
requestGeneration === this.myRequestsRequestGeneration &&
|
||||
list === this.myRequestsList
|
||||
if (!isCurrentRequest) {
|
||||
if (
|
||||
deviceScopeVersion === this.deviceScopeVersion &&
|
||||
requestGeneration === this.myRequestsRequestGeneration
|
||||
) {
|
||||
if (append) this.myRequestsLoadingMore = false
|
||||
else this.myRequestsLoading = false
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (append) this.myRequestsLoadingMore = false
|
||||
else this.myRequestsLoading = false
|
||||
if (!response.success || !response.data) {
|
||||
if (append) {
|
||||
this.myRequestsAppendError = response.error ?? 'request_failed'
|
||||
return false
|
||||
}
|
||||
this.myRequestsError = response.error ?? 'request_failed'
|
||||
if (!append) {
|
||||
this.myRequests = []
|
||||
this.myRequestsNextCursor = null
|
||||
if (
|
||||
response.error === 'anonymous_sim' ||
|
||||
response.error === 'device_not_found' ||
|
||||
response.error === 'no_sim'
|
||||
) {
|
||||
this.customerUnreadCount = 0
|
||||
}
|
||||
this.myRequests = []
|
||||
this.myRequestsNextCursor = null
|
||||
if (
|
||||
response.error === 'anonymous_sim' ||
|
||||
response.error === 'device_not_found' ||
|
||||
response.error === 'no_sim'
|
||||
) {
|
||||
this.customerUnreadCount = 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -245,7 +319,8 @@ export const useCompaniesStore = defineStore('companies', {
|
||||
: response.data.requests
|
||||
this.myRequestsNextCursor = response.data.nextCursor
|
||||
this.customerUnreadCount = Math.max(0, response.data.unreadCount)
|
||||
this.myRequestsError = ''
|
||||
if (append) this.myRequestsAppendError = ''
|
||||
else this.myRequestsError = ''
|
||||
return true
|
||||
},
|
||||
async loadRequest(requestId: string): Promise<boolean> {
|
||||
@@ -524,8 +599,10 @@ export const useCompaniesStore = defineStore('companies', {
|
||||
},
|
||||
resetDeviceScope(): void {
|
||||
this.deviceScopeVersion += 1
|
||||
this.myRequestsRequestGeneration += 1
|
||||
this.customerUnreadCount = 0
|
||||
this.myRequests = []
|
||||
this.myRequestsAppendError = ''
|
||||
this.myRequestsError = ''
|
||||
this.myRequestsLoaded = false
|
||||
this.myRequestsLoading = false
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user