diff --git a/src/composables/onResize.ts b/src/composables/onResize.ts new file mode 100644 index 000000000..3a47a575f --- /dev/null +++ b/src/composables/onResize.ts @@ -0,0 +1,70 @@ +type Options = { + immediate?: boolean +} + +/** + * run callback on resize windows + * + * @function + * @name onResize + * @kind variable + * @param {() => void} callback + * @param {Options} options? + * @returns {void} + * @exports + */ +export const onResize = (callback: () => void, options: Options = {}) => { + onMounted(() => { + if (import.meta.client) { + window.addEventListener('resize', callback) + if (options.immediate) { + callback() + } + } + }) + + onUnmounted(() => { + if (import.meta.client) { + window.removeEventListener('resize', callback) + } + }) +} + +/** + * run callback when mediaQuery change states + * + * @function + * @name onMediaChange + * @kind variable + * @param {string} media + * @param {(matche: boolean) => void} callback + * @param {Options} options? + * @returns {void} + * @exports + */ +export const onMediaChange = ( + media: string, + callback: (matche: boolean) => void, + options: Options = {} +) => { + const eventer = window?.matchMedia(media) + + const proxyCallback = () => { + callback(!!eventer.matches) + } + + onMounted(() => { + if (import.meta.client) { + eventer.addEventListener('change', proxyCallback) + if (options.immediate) { + proxyCallback() + } + } + }) + + onUnmounted(() => { + if (import.meta.client) { + eventer.removeEventListener('change', proxyCallback) + } + }) +} diff --git a/src/composables/useAPI.ts b/src/composables/useAPI.ts index cc44234c5..1b4535158 100644 --- a/src/composables/useAPI.ts +++ b/src/composables/useAPI.ts @@ -9,9 +9,12 @@ export const defaultOptions = () => { const localStorage = _localStorage const runtimeConfig = useRuntimeConfig() const usersStore = useUsers() + const headers = useRequestHeaders(['cookie']) + return { baseURL: runtimeConfig.public.appApiUrl + runtimeConfig.public.appApiDefaultVersion + '/', method: 'GET', + headers, onRequest({ options }) { if (import.meta.client) { const accessToken = usersStore.accessToken // localStorage?.getItem('ACCESS_TOKEN') diff --git a/src/composables/useAPI2.ts b/src/composables/useAPI2.ts index 971faef7d..3ebad4ae4 100644 --- a/src/composables/useAPI2.ts +++ b/src/composables/useAPI2.ts @@ -1,11 +1,19 @@ import { merge } from 'es-toolkit' import { defaultOptions } from '@/composables/useAPI' +import useLoadingFromStatus from '@/composables/useLoadingFromStatus' type Params = Parameters const useAPI2 = (url: Params[0], options: Params[1] = {}) => { const _options = merge(defaultOptions(), options) - return useFetch(url, _options as any) + const { status, ...rest } = useFetch(url, _options as any) + const loading = useLoadingFromStatus(status) + + return { + ...rest, + status, + loading, + } } export default useAPI2 diff --git a/src/composables/useAsyncAPI.ts b/src/composables/useAsyncAPI.ts new file mode 100644 index 000000000..716b2dac2 --- /dev/null +++ b/src/composables/useAsyncAPI.ts @@ -0,0 +1,126 @@ +import useLoadingFromStatus from '@/composables/useLoadingFromStatus' +import { isNil } from 'es-toolkit' + +type AsyncHandler = { + ctx?: Parameters['1']>[0] + config: { + query?: any + } +} + +export type AsyncConfig = Parameters< + typeof useAsyncData +>['2'] & { + translate?: (data: DataT) => Result + query?: object + checkArgs?: boolean +} + +export type AsyncParameters = [ + Parameters>['0'], + (obj: AsyncHandler) => ReturnType>['1']>, + AsyncConfig?, +] + +type AsyncReturn = Omit< + ReturnType>, + 'data' +> & { + data: Result extends undefined + ? ReturnType>['data'] + : Result + isLoading: ComputedRef +} + +/** + * wrapper around useAsyncData (for watch local change TODO !) + * + * @constant + * @name useAsyncAPI + * @kind variable + * @exports + */ +export default function useAsyncAPI( + ...params: AsyncParameters +): AsyncReturn { + /* + TODO: this code refresh request when local change (no more send) + multiple value for same keys like "title_en", "title_es", "title_fr" ..ect + const { locale } = useNuxtI18n() + params[2].watch = [...(params[2].watch || []), locale] + */ + params[2] ??= {} + params[2].watch ??= [] + if ( + params[2].query && + (isRef(params[2].query) || isReactive(params[2].query)) && + !params[2].watch.includes(params[2].query) + ) { + params[2].watch.push(params[2].query) + } + + let immediate = true + if (params[2].immediate === false) { + immediate = true + } else { + params[2].immediate = false + } + + const checkArgs = computed(() => { + return params[2].watch.map((v) => unref(v)).filter((v) => isNil(v)).length === 0 + }) + + const { status, data, ...res } = useAsyncData( + params[0], + ({}) => { + if (!checkArgs.value) { + return null + } + const conf = {} as AsyncHandler['config'] + if (params[2].query) { + conf.query = unref(params[2].query) + } else { + conf.query ??= {} + } + return params[1]({ config: conf }) + }, + { + ...params[2], + } + ) + const isLoading = useLoadingFromStatus(status) + + // @ts-expect-error 2345 todo check why + const dataWrapped = params[2]?.translate ? params[2]?.translate(data) : data + + const results = { + ...res, + status, + isLoading, + data: dataWrapped, + } + + if (immediate) { + watch( + checkArgs, + (newValue) => { + if (newValue) { + results.refresh() + } + }, + { immediate: true } + ) + } + + // log error only in dev + if (import.meta.dev) { + watchEffect(() => { + if (results.error.value) { + console.error(params[0], results.error.value) + } + }) + } + + // @ts-expect-error 2322 todo check why + return results +} diff --git a/src/composables/useAsyncPaginationAPI.ts b/src/composables/useAsyncPaginationAPI.ts new file mode 100644 index 000000000..d2a5dc0ce --- /dev/null +++ b/src/composables/useAsyncPaginationAPI.ts @@ -0,0 +1,103 @@ +import { + Pagination, + PaginationQuery, + PaginationResult, + paginationConfig, + usePagination, +} from '@/composables/usePagination' +import { omit } from 'es-toolkit' + +import type { AsyncConfig } from './useAsyncAPI' +import useAsyncAPI from './useAsyncAPI' + +type AsyncPaginationHandler = { + ctx?: Parameters['1']>[0] + config: { + query: PaginationQuery & { + [key: string]: any + } + } +} + +type AsyncPaginationConfig = Omit< + AsyncConfig, + 'transform' +> & { + // default configuration of paginations + paginationConfig?: paginationConfig + // method to transform data + transform?: (data: DataT) => DataT + translate?: (data: DataT['results']) => Result +} + +type AsyncPaginationParameters = [ + Parameters>['0'], + ( + obj: AsyncPaginationHandler + ) => ReturnType>['1']>, + AsyncPaginationConfig?, +] + +type AsyncPaginationcReturn = Omit< + ReturnType>, + 'data' +> & { + pagination: Pagination + data: Result extends undefined ? ComputedRef : Result +} + +/** + * you can change/set page automaticlym and the request auto refresh it + * a wrapped composable under useAsyncData and usePagination + * + * @constant + * @kind variable + * @exports + */ +export default function useAsyncPaginationAPI( + ...params: AsyncPaginationParameters, PaginationResult, Result> +): AsyncPaginationcReturn, PaginationResult, Result> { + const paginationData = ref() + const pagination = usePagination(paginationData, params[2]?.paginationConfig) + + // add paginations in query + const key = computed( + () => `${unref(params[0])}::${pagination.current.value}::${pagination.limit.value}` + ) + + const rest = useAsyncAPI( + key, + ({ config }) => { + return params[1]({ + config: { + query: { + ...config.query, + ...pagination.query(), + }, + }, + }) + }, + { + ...((omit(params[2] ?? {}, ['transform', 'translate']) ?? {}) as Omit< + AsyncConfig, PaginationResult, Result>, + 'translate' + >), + watch: [...(params[2]?.watch || []), pagination.current, pagination.limit], + deep: false, + } + ) + + const data = rest.data + watch(data, () => (paginationData.value = rest.data.value)) + + const results = computed(() => data.value?.results) + // @ts-expect-error betwwen translate and raw + const subData = params[2]?.translate ? params[2]?.translate(results) : results + + return { + ...rest, + // @ts-expect-error betwwen translate and raw + data: subData, + pagination, + } +} diff --git a/src/composables/useForm.ts b/src/composables/useForm.ts index 18eea374e..3f32a7c73 100644 --- a/src/composables/useForm.ts +++ b/src/composables/useForm.ts @@ -1,18 +1,20 @@ import useValidate from '@vuelidate/core' -import { debounce } from 'es-toolkit' -type OptionsForm = { - default?: object +type OptionsForm = { + default?: T rules?: object validateTimeout?: number - onClean: (data: object) => object + onClean?: (data: T) => CleanResult + model?: Ref } -type UseFormResult = { - form: Ref +export type UseFormResult = { + form: Ref isValid: Ref - errors: Ref - cleanedData: null | Ref + errors: ComputedRef<{ + [key: string]: any + }> + cleanedData: null | Ref } const onClean = (d) => d @@ -26,19 +28,27 @@ const onClean = (d) => d * @param {OptionsForm} options? * @returns {UseFormResult} */ -const useForm = (options: OptionsForm = { onClean }): UseFormResult => { - const form = ref({ ...(options.default ?? {}) }) +const useForm = ( + options: OptionsForm = { onClean } +): UseFormResult => { + const def = { + ...(options.default ?? {}), + ...unref(options.model?.value ?? {}), + } as T + + const form = ref(def) as Ref const _onClean = options.onClean ?? onClean const isValid = ref(false) const v$ = useValidate(options.rules ?? {}, form) - // debounce validate to optimize check const validate = () => v$.value.$validate().then((v) => (isValid.value = v)) - const debounceValidate = debounce(validate, options.validateTimeout ?? 200) - watch(form, () => debounceValidate(), { deep: true, immediate: true }) + // const debounceValidate = debounce(validate, options.validateTimeout ?? 50) + watch(form, () => validate(), { deep: true, immediate: true }) - const errors = computed(() => { + const errors = computed<{ + [key: string]: string[] + }>(() => { const err = {} Object.keys(form.value).forEach((k) => { if (v$.value[k]?.$errors) { @@ -48,13 +58,24 @@ const useForm = (options: OptionsForm = { onClean }): UseFormResult => { return err }) - // clean data (before send to backend) - const cleanedData = computed(() => { - if (!isValid.value) { - return null - } - return _onClean(form.value) - }) + const cleanedData = ref() + + watch( + [form, isValid], + () => { + const formContent = { ...toRaw(form.value) } + + let cleanded = null + if (isValid.value) { + cleanded = _onClean(formContent) + } + if (options.model) { + options.model.value = cleanded + } + cleanedData.value = cleanded + }, + { deep: true, immediate: true } + ) return { form, diff --git a/src/composables/useLoadingFromStatus.ts b/src/composables/useLoadingFromStatus.ts new file mode 100644 index 000000000..b5fd7a7e9 --- /dev/null +++ b/src/composables/useLoadingFromStatus.ts @@ -0,0 +1,19 @@ +/** + * convert status from useFetch to boolean + * + * @function + * @name useLoadingFromStatus + * @kind variable + * @param {any} status + * @returns {globalThis.ComputedRef} + */ + +import { AsyncDataRequestStatus } from 'nuxt/app' + +const useLoadingFromStatus = (status: Ref) => { + const loading = computed(() => ['idle', 'pending'].includes(status.value)) + + return loading +} + +export default useLoadingFromStatus diff --git a/src/composables/useModal.ts b/src/composables/useModal.ts new file mode 100644 index 000000000..24d22b246 --- /dev/null +++ b/src/composables/useModal.ts @@ -0,0 +1,71 @@ +/** + * interacte with multiple modals + * + * @constant + * @name useModals + * @kind variable + * @type {(defaultValue: Partial) => { stateModal: [Partial] extends [globalThis.Ref] ? IfAny & Partial, globalThis.Ref & Partial, globalThis.Ref & Partial>, globalThis.Ref & Partial> : globalThis.Ref<...>; openModal: (key: K) => void; closeModal: (key: K) => void; toggleModal: (key: K) => void; }} + * @exports + */ +export const useModals = ( + defaultValue: Partial +) => { + const stateModals = ref>(defaultValue ?? {}) + const openModals = (...keys: K[]) => { + keys.forEach((k) => (stateModals.value[k] = true)) + } + + const closeModals = (...keys: K[]) => { + keys.forEach((k) => (stateModals.value[k] = false)) + } + + /** + * toggle value in object ( if exists with the same value, remove it else set it) + */ + const toggleModals = (...keys: K[]) => { + keys.forEach((k) => { + if (stateModals.value[k]) { + closeModals(k) + } else { + openModals(k) + } + }) + } + + return { + stateModals, + openModals, + closeModals, + toggleModals, + } +} + +/** + * interact with one modal + * + * @function + * @name useModal + * @kind variable + * @param {boolean) => { stateModal} initialState? + * @param {() => void openModal} globalThis.ComputedRef closeModal + * @param {(} () => void toggleModal + * @returns {void; }} + * @exports + */ +export const useModal = (initialState: boolean = false) => { + const { stateModals, closeModals, openModals, toggleModals } = useModals({ base: initialState }) + + const stateModal = computed(() => stateModals.value.base) + const closeModal = () => closeModals('base') + const openModal = () => openModals('base') + const toggleModal = () => toggleModals('base') + + return { + stateModal, + closeModal, + openModal, + toggleModal, + } +} + +export default useModal diff --git a/src/composables/useOrganizationCode.ts b/src/composables/useOrganizationCode.ts index f65fcf8e0..e75de7ae0 100644 --- a/src/composables/useOrganizationCode.ts +++ b/src/composables/useOrganizationCode.ts @@ -1,7 +1,15 @@ import { useRuntimeConfig } from '#imports' import useOrganizationsStore from '@/stores/useOrganizations' -const useOrganizationCode = () => { +/** + * return current organizationCode + * + * @function + * @name useOrganizationCode + * @kind variable + * @returns {string} + */ +const useOrganizationCode = (): string => { const store = useOrganizationsStore() // If no organization set in the param use default one // TODO check why rootState.organizations.current is sometimes null diff --git a/src/composables/usePagination.ts b/src/composables/usePagination.ts index babf9bc74..eb9984d17 100644 --- a/src/composables/usePagination.ts +++ b/src/composables/usePagination.ts @@ -34,10 +34,10 @@ export type PaginationResult = { export type PaginationQuery = { limit: number - offset?: number + offset: number } -const DEFAULT_Pagination_LIMIT = 100 +export const DEFAULT_PAGINATION_LIMIT = 10 export type paginationConfig = { limit?: number offset?: number @@ -45,6 +45,8 @@ export type paginationConfig = { export type Pagination = { current: Ref + limit: Ref + offset: ComputedRef total: Ref count: Ref setPage: (page: number) => boolean @@ -68,12 +70,13 @@ export const usePagination = ( const current = ref(1) const total = ref(0) const count = ref(0) - const limit = paginationConfig.limit ?? DEFAULT_Pagination_LIMIT + const limit = ref(paginationConfig.limit ?? DEFAULT_PAGINATION_LIMIT) + const offset = computed(() => Math.max((current.value - 1) * limit.value, 0)) watch( results, () => { - current.value = results.value?.current_page ?? 0 + current.value = results.value?.current_page || 1 total.value = results.value?.total_page ?? 0 count.value = results.value?.count ?? 0 }, @@ -115,15 +118,16 @@ export const usePagination = ( const query = (): PaginationQuery => { // page - 1 for first page - const offset = (current.value - 1) * limit return { - offset, - limit, + offset: offset.value, + limit: limit.value, } } return { current, + limit, + offset, setPage, total, count, diff --git a/src/composables/usePatatoids.ts b/src/composables/usePatatoids.ts index 7bc227581..616e8c1c9 100644 --- a/src/composables/usePatatoids.ts +++ b/src/composables/usePatatoids.ts @@ -1,8 +1,13 @@ +import { usePublicURL } from '@/composables/usePublic' + const DEFAULT_PATATOID = [1, 2, 3, 4, 5, 6].map( (index) => `/patatoids-project/Patatoid-${index}.png` ) const DEFAULT_USER_PATATOID = DEFAULT_PATATOID[0] +const DEFAULT_GROUP_PATATOID = DEFAULT_PATATOID[1] +const DEFAULT_PROJECT_PATATOID = DEFAULT_PATATOID[2] +const DEFAULT_IMAGE_PATATOID = DEFAULT_PATATOID[3] /** * return default 6 patatoids @@ -13,8 +18,15 @@ const DEFAULT_USER_PATATOID = DEFAULT_PATATOID[0] * @returns {string[]} */ const usePatatoids = () => { - const runtimeConfig = useRuntimeConfig() - return DEFAULT_PATATOID.map((url) => `${runtimeConfig.public.appPublicBinariesPrefix}${url}`) + return DEFAULT_PATATOID.map((url) => usePublicURL(url)) } -export { usePatatoids, DEFAULT_USER_PATATOID, DEFAULT_PATATOID } +export { + usePatatoids, + usePatatoid, + DEFAULT_PATATOID, + DEFAULT_USER_PATATOID, + DEFAULT_GROUP_PATATOID, + DEFAULT_PROJECT_PATATOID, + DEFAULT_IMAGE_PATATOID, +} diff --git a/src/composables/usePublic.ts b/src/composables/usePublic.ts new file mode 100644 index 000000000..68e4f3877 --- /dev/null +++ b/src/composables/usePublic.ts @@ -0,0 +1,14 @@ +/** + * prefix url with backend s3 url + * + * @function + * @name usePublicURL + * @kind variable + * @param {any} url + * @returns {string} + * @exports + */ +export const usePublicURL = (url: string) => { + const runtimeConfig = useRuntimeConfig() + return `${runtimeConfig.public.appPublicBinariesPrefix}${url}` +} diff --git a/src/composables/useQuery.ts b/src/composables/useQuery.ts index 2e9328254..5dfe39c92 100644 --- a/src/composables/useQuery.ts +++ b/src/composables/useQuery.ts @@ -4,10 +4,10 @@ * @constant * @name useQuery * @kind variable - * @type {(defaultValue: Partial) => { query: Reactive>; setQuery: (key: K, value: DataQuery[K]) => void; removeQuery: (key: K) => void; toggleQuery: (key: K, value: DataQuery[K]) => void; }} + * @type {(defaultValue: Partial) => { query: Reactive>; setQuery: (key: K, value: DataQuery[K]) => void; removeQuery: (key: K) => void; toggleQuery: (key: K, value: DataQuery[K]) => void; }} * @exports */ -export const useQuery = ( +export const useQuery = ( defaultValue: Partial ) => { const query = reactive>(defaultValue ?? {}) diff --git a/src/composables/useScrollToTab.ts b/src/composables/useScrollToTab.ts index 499d3037a..230dbff64 100644 --- a/src/composables/useScrollToTab.ts +++ b/src/composables/useScrollToTab.ts @@ -15,3 +15,20 @@ export default function useScrollToTab() { } }) } + +/** + * function to scroll to hash element (id="#myhash") + * + * @function + * @name scrollToHash + * @kind variable + * @param {string} hash + * @returns {void} + * @exports + */ +export const scrollToHash = (hash: string) => { + const target = document.getElementById(hash) + if (target) { + funct.scrollTo(target) + } +} diff --git a/src/composables/useToggleableNavPanel.ts b/src/composables/useToggleableNavPanel.ts index a655d8d8f..496199ee7 100644 --- a/src/composables/useToggleableNavPanel.ts +++ b/src/composables/useToggleableNavPanel.ts @@ -1,3 +1,4 @@ +// TODO: use onMediaChange export default function useToggleableNavPanel(uniqueId, breakpoint = 768) { const isNavCollapsed = ref(window?.innerWidth < breakpoint) const toggleNavPanel = () => { diff --git a/src/composables/useUniqueId.ts b/src/composables/useUniqueId.ts new file mode 100644 index 000000000..d05ce1e07 --- /dev/null +++ b/src/composables/useUniqueId.ts @@ -0,0 +1,12 @@ +/** + * generate unique Id + * + * @function + * @name useUniqueId + * @kind variable + * @returns {string} + * @exports + */ +export const useUniqueId = () => { + return (Math.random() + 1).toString(36).substring(7) +} diff --git a/src/composables/useViewportWidth.ts b/src/composables/useViewportWidth.ts index 815804b3e..0b1f5805c 100644 --- a/src/composables/useViewportWidth.ts +++ b/src/composables/useViewportWidth.ts @@ -1,36 +1,19 @@ -export default function useViewportWidth() { - const viewportWidth = useState(() => 0) - - const getWindowWidth = () => { - if (import.meta.client) { - viewportWidth.value = window.innerWidth - } - } - - const debouncedGetWindowWidth = getWindowWidth - - onMounted(() => { - if (import.meta.client) { - getWindowWidth() - window.addEventListener('resize', debouncedGetWindowWidth) - } - }) - - onUnmounted(() => { - if (import.meta.client) window.removeEventListener('resize', debouncedGetWindowWidth) - }) - - const isMobile = computed(() => { - return viewportWidth.value <= 414 - }) +import { onMediaChange } from '@/composables/onResize' - const isTablet = computed(() => { - return viewportWidth.value > 414 && viewportWidth.value < 1024 - }) - - const isDesktop = computed(() => { - return viewportWidth.value > 1024 +export default function useViewportWidth() { + const isMobile = ref(false) + const isTablet = ref(false) + const isDesktop = ref(false) + + onMediaChange('(max-width: 414px)', (matches) => (isMobile.value = matches), { immediate: true }) + onMediaChange( + '(min-width: 414px) and (max-width: 1024px)', + (matches) => (isTablet.value = matches), + { immediate: true } + ) + onMediaChange('(min-width: 1024px)', (matches) => (isDesktop.value = matches), { + immediate: true, }) - return { viewportWidth, isMobile, isTablet, isDesktop } + return { isMobile, isTablet, isDesktop } } diff --git a/tests/unit/composables/useAsyncAPI.test.ts b/tests/unit/composables/useAsyncAPI.test.ts new file mode 100644 index 000000000..076084efa --- /dev/null +++ b/tests/unit/composables/useAsyncAPI.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from 'vitest' +import { ref, computed } from 'vue' +import useAsyncAPI from '@/composables/useAsyncAPI' +import flushPromises from 'flush-promises' + +describe('useAsyncAPI composable', () => { + it('default', async () => { + const { isLoading, data } = useAsyncAPI('default', async () => 'data') + + expect(isLoading.value).toBe(true) + await flushPromises() + + expect(isLoading.value).toBe(false) + expect(data.value).toBe('data') + }) + + it('watch query', async () => { + const query = ref({ page: 0 }) + + const { data } = useAsyncAPI( + 'watch query', + async ({ config }) => { + return `data-${config.query.page}` + }, + { query } + ) + + await flushPromises() + expect(data.value).toBe('data-0') + + query.value = { page: 666 } + await flushPromises() + expect(data.value).toBe('data-666') + }) + + it('watch with undefined', async () => { + const query = ref(null) + + const { data } = useAsyncAPI( + 'watch with undefined', + async ({ config }) => { + return `data-${config.query.page}` + }, + { query } + ) + + await flushPromises() + // query is null we dont run async function + expect(data.value).toBeNull() + + query.value = { page: 666 } + await flushPromises() + expect(data.value).toBe('data-666') + }) + + it('watch with multiple undefined', async () => { + const query = ref(null) + const groupId = ref(undefined) + + const { data } = useAsyncAPI( + 'watch with multiple undefined', + async ({ config }) => { + console.log('test3') + return `data-${config.query.page}` + }, + { query, watch: [groupId] } + ) + + await flushPromises() + // query is null we dont run async function + expect(data.value).toBeNull() + + query.value = { page: 666 } + await flushPromises() + expect(data.value).toBeNull() + + groupId.value = '55' + await flushPromises() + expect(data.value).toBe('data-666') + }) + + it('with translate', async () => { + const translate = (result) => { + return computed(() => `${result.value}--translate`) + } + + const query = reactive({ page: 0 }) + const { data } = useAsyncAPI( + 'with translate', + async ({ config }) => { + return `data-${config.query.page}` + }, + { query, translate } + ) + + expect(data.value).toBe('null--translate') + + await flushPromises() + expect(data.value).toBe('data-0--translate') + + query.page = 666 + await flushPromises() + expect(data.value).toBe('data-666--translate') + }) +}) diff --git a/tests/unit/composables/useAsyncPaginationAPI.test.ts b/tests/unit/composables/useAsyncPaginationAPI.test.ts new file mode 100644 index 000000000..53427633b --- /dev/null +++ b/tests/unit/composables/useAsyncPaginationAPI.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest' +import { computed } from 'vue' +import useAsyncPaginationAPI from '@/composables/useAsyncPaginationAPI' +import flushPromises from 'flush-promises' + +// all other tests is on "useAsyncAPI" + +describe('useAsyncPaginationAPI composable', () => { + it('default', async () => { + const { pagination, data } = useAsyncPaginationAPI( + 'default', + async ({ config }) => { + return { + count: 66, + current_page: config.query.offset / config.query.limit + 1, + results: [config.query], + } + }, + { + paginationConfig: { + limit: 7, + }, + } + ) + + await flushPromises() + expect(data.value).toStrictEqual([ + { + limit: 7, + offset: 0, // current page 1 + }, + ]) + + pagination.next() + await flushPromises() + + expect(data.value).toStrictEqual([ + { + limit: 7, + offset: 7, // current page 2 + }, + ]) + }) + + it('translate', async () => { + const translate = (results) => { + return computed(() => `${results.value[0]}--translate`) + } + + const { pagination, data } = useAsyncPaginationAPI( + 'translate', + async ({ config }) => { + return { + count: 66, + current_page: config.query.offset / config.query.limit + 1, + results: [`result:${config.query.offset}:${config.query.limit}`], + } + }, + { + translate, + paginationConfig: { + limit: 7, + }, + } + ) + + await flushPromises() + expect(data.value).toEqual(`result:0:7--translate`) + + pagination.next() + await flushPromises() + + expect(data.value).toEqual(`result:7:7--translate`) + }) +}) diff --git a/tests/unit/composables/useForm.test.js b/tests/unit/composables/useForm.test.js index dd4f5d504..515306610 100644 --- a/tests/unit/composables/useForm.test.js +++ b/tests/unit/composables/useForm.test.js @@ -103,4 +103,28 @@ describe('useForm', () => { expect(errors.value.childrens).toBeTruthy() expect(cleanedData.value).toBeNull() }) + + it('with v-model', async () => { + const model = ref({ + name: 'jaques', + }) + + const onClean = (data) => { + return { + name: 'newName', + } + } + + const rules = { + name: { required, minLengthValue: minLength(4) }, + } + const { form, cleanedData } = useForm({ rules, model, onClean }) + + expect(form.value.name).toEqual('jaques') + await delay(10) + expect(cleanedData.value.name).toEqual('newName') + form.value.name = 'li' + await delay(10) + expect(cleanedData.value).toBe(null) + }) }) diff --git a/tests/unit/composables/useLoadingFromStatus.test.ts b/tests/unit/composables/useLoadingFromStatus.test.ts new file mode 100644 index 000000000..a00216874 --- /dev/null +++ b/tests/unit/composables/useLoadingFromStatus.test.ts @@ -0,0 +1,24 @@ +import { flushPromises } from '@vue/test-utils' +import { AsyncDataRequestStatus } from 'nuxt/app' +import { describe, it, expect } from 'vitest' + +describe('useLoadingFromStatus', () => { + it('isLoading', async () => { + const status = ref('success') + const isLoading = useLoadingFromStatus(status) + await flushPromises() + expect(isLoading.value).toBe(false) + + status.value = 'error' + await flushPromises() + expect(isLoading.value).toBe(false) + + status.value = 'pending' + await flushPromises() + expect(isLoading.value).toBe(true) + + status.value = 'idle' + await flushPromises() + expect(isLoading.value).toBe(true) + }) +}) diff --git a/tests/unit/composables/useModal.test.ts b/tests/unit/composables/useModal.test.ts new file mode 100644 index 000000000..f61edb577 --- /dev/null +++ b/tests/unit/composables/useModal.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest' +import { ref, computed } from 'vue' +import useAsyncAPI from '@/composables/useAsyncAPI' +import flushPromises from 'flush-promises' + +describe('useModals composable', () => { + it('default', () => { + const { stateModals, closeModals, openModals, toggleModals } = useModals({ + drawer: true, + modal: false, + }) + expect(stateModals.value.drawer).toBe(true) + expect(stateModals.value.modal).toBe(false) + + toggleModals('drawer', 'modal') + expect(stateModals.value.drawer).toBe(false) + expect(stateModals.value.modal).toBe(true) + + closeModals('modal') + expect(stateModals.value.modal).toBe(false) + + openModals('modal') + expect(stateModals.value.modal).toBe(true) + }) +}) + +describe('useModal composable', () => { + it('default', () => { + const { stateModal, closeModal, openModal, toggleModal } = useModal(false) + expect(stateModal.value).toBe(false) + + toggleModal() + expect(stateModal.value).toBe(true) + + closeModal() + expect(stateModal.value).toBe(false) + + openModal() + expect(stateModal.value).toBe(true) + }) +})