Skip to content

Commit 29a434b

Browse files
committed
refactor: reorganize control components and introduce new input types
1 parent bc9c35f commit 29a434b

40 files changed

+1747
-632
lines changed

.github/workflows/ci.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CI
2+
permissions:
3+
contents: read
4+
5+
on:
6+
push:
7+
branches: [main, develop]
8+
pull_request:
9+
branches: [main]
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
15+
strategy:
16+
matrix:
17+
node-version: [18.x, 20.x]
18+
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Setup Bun
24+
uses: oven-sh/setup-bun@v1
25+
with:
26+
bun-version: latest
27+
28+
- name: Install dependencies
29+
run: bun install
30+
31+
- name: Run tests with coverage
32+
run: bun test --coverage --coverage-reporter lcov
33+
34+
- name: Upload coverage to Codecov
35+
uses: codecov/codecov-action@v5
36+
with:
37+
token: ${{ secrets.CODECOV_TOKEN }}
38+
slug: The-Software-Compagny/parser_ldap_rfc4512
39+
files: ./coverage/lcov.info
40+
flags: unittests
41+
name: codecov-umbrella
42+
fail_ci_if_error: false
43+
44+
build:
45+
runs-on: ubuntu-latest
46+
needs: test
47+
48+
steps:
49+
- name: Checkout code
50+
uses: actions/checkout@v4
51+
52+
- name: Setup Bun
53+
uses: oven-sh/setup-bun@v1
54+
with:
55+
bun-version: latest
56+
57+
- name: Install dependencies
58+
run: bun install
59+
60+
- name: Build project
61+
run: bun run build

playground/examples/items/i18n.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ export const i18n = {
5858
label: 'Nom',
5959
description: 'Veuillez entrer votre nom',
6060
},
61+
nationality: {
62+
label: 'Nationalité',
63+
description: 'Veuillez sélectionner votre nationalité',
64+
},
65+
vegetarian: {
66+
label: 'Végétarien',
67+
description: 'Cochez si vous êtes végétarien',
68+
},
69+
additionalInformationLabel: 'Informations supplémentaires',
6170
basicInfoGroup: 'Informations de base',
6271
},
6372
en: {

src/advanced/wysiwyg.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,12 +423,12 @@ export const entry: JsonFormsRendererRegistryEntry = {
423423
}
424424
425425
.q-field__control {
426-
border-radius: 0 0 0.75rem 0.75rem;
426+
border-radius: 0 0 0.25rem 0.25rem;
427427
}
428428
429429
.tiptap.ProseMirror {
430430
height: 100%;
431-
padding: 10px;
431+
padding: 5px 15px;
432432
}
433433
434434
.ProseMirror-focused {

src/composables/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
11
export * from './useHashState'
2+
export * from './useNumericControl'
3+
export * from './useInputControl'
4+
export * from './usePasswordControl'
5+
export * from './useSliderControl'
6+
export * from './useRadioGroupControl'
7+
export * from './useBooleanControl'
8+
export * from './useEnumSuggestionControl'
9+
export * from './useTextareaControl'
10+
export * from './useAutocompleteControl'
11+
export * from './useDateControl'
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import { computed, ref } from 'vue'
2+
import { get, isArray } from 'radash'
3+
import { useQuasarControl } from '../utils'
4+
import type { useJsonFormsEnumControl } from '@jsonforms/vue'
5+
import {
6+
createEnumAdaptTarget,
7+
normalizeSuggestions,
8+
} from './useEnumSuggestionControl'
9+
10+
export interface AutocompleteApiConfig {
11+
url: string
12+
base?: string
13+
queryKey?: string
14+
labelKey?: string
15+
valueKey?: string
16+
itemsPath?: string
17+
params?: Record<string, unknown>
18+
headers?: Record<string, string>
19+
}
20+
21+
type JsonFormsEnumControl = ReturnType<typeof useJsonFormsEnumControl>
22+
23+
type UseAutocompleteControlOptions = {
24+
jsonFormsControl: JsonFormsEnumControl
25+
clearValue: unknown
26+
debounceWait?: number
27+
defaultMinLength?: number
28+
}
29+
30+
export const resolveAutocompleteMinLength = (
31+
uiOptions: any,
32+
appliedOptions: any,
33+
fallback: number,
34+
): number => {
35+
const uiValue = Number(uiOptions?.minLength)
36+
if (!Number.isNaN(uiValue) && uiValue > 0) {
37+
return uiValue
38+
}
39+
40+
const appliedValue = Number(appliedOptions?.minLength)
41+
if (!Number.isNaN(appliedValue) && appliedValue > 0) {
42+
return appliedValue
43+
}
44+
45+
return fallback
46+
}
47+
48+
export const mapSuggestionsToOptions = (
49+
suggestions: string[] | undefined,
50+
): Array<{ label: string; value: string }> | undefined => {
51+
if (!suggestions?.length) {
52+
return undefined
53+
}
54+
55+
return suggestions.map((label) => ({ label, value: label }))
56+
}
57+
58+
export const extractAutocompleteApiConfig = (
59+
uiOptions: any,
60+
appliedOptions: any,
61+
): AutocompleteApiConfig | undefined => {
62+
const api = uiOptions?.api ?? appliedOptions?.api
63+
if (!api?.url) {
64+
return undefined
65+
}
66+
67+
return api
68+
}
69+
70+
export const buildAutocompleteRequest = (
71+
api: AutocompleteApiConfig,
72+
search: string,
73+
): { url: string; headers: Record<string, string> } => {
74+
const baseUrl = api.base ? new URL(api.url, api.base) : new URL(api.url)
75+
const queryKey = api.queryKey ?? 'q'
76+
baseUrl.searchParams.set(queryKey, search)
77+
78+
if (api.params) {
79+
Object.entries(api.params).forEach(([key, value]) => {
80+
baseUrl.searchParams.set(key, String(value))
81+
})
82+
}
83+
84+
return {
85+
url: baseUrl.toString(),
86+
headers: api.headers ?? {},
87+
}
88+
}
89+
90+
export const resolveFetchedOptions = (
91+
items: any[],
92+
api: AutocompleteApiConfig,
93+
): Array<{ label: string; value: unknown }> => {
94+
const labelKey = api.labelKey ?? 'label'
95+
const valueKey = api.valueKey ?? 'value'
96+
97+
return items.map((item) => {
98+
const label = get(item, labelKey)
99+
const value = get(item, valueKey)
100+
101+
return {
102+
label: String(label ?? item?.toString?.() ?? ''),
103+
value: value ?? item,
104+
}
105+
})
106+
}
107+
108+
const isArrayOfOptions = (options: unknown): options is any[] => {
109+
return Array.isArray(options)
110+
}
111+
112+
const getStaticOptions = (control: JsonFormsEnumControl['control']['value']) => {
113+
return isArrayOfOptions(control.options) ? control.options : []
114+
}
115+
116+
const filterOptionsBySearch = (options: any[], search: string) => {
117+
const lowered = search.toLowerCase()
118+
119+
return options.filter((option) => {
120+
if (typeof option === 'string') {
121+
return option.toLowerCase().includes(lowered)
122+
}
123+
124+
const label = option?.label ?? option?.toString?.()
125+
return typeof label === 'string' && label.toLowerCase().includes(lowered)
126+
})
127+
}
128+
129+
export const useAutocompleteControl = ({
130+
jsonFormsControl,
131+
clearValue,
132+
debounceWait = 100,
133+
defaultMinLength = 3,
134+
}: UseAutocompleteControlOptions) => {
135+
const adaptTarget = createEnumAdaptTarget(clearValue)
136+
const control = useQuasarControl(jsonFormsControl, adaptTarget, debounceWait)
137+
138+
const optionsList = ref<any[]>([])
139+
const abortController = ref<AbortController | null>(null)
140+
141+
const suggestions = computed(() => {
142+
const normalized = normalizeSuggestions(
143+
control.control.value.uischema.options?.suggestion,
144+
)
145+
146+
return mapSuggestionsToOptions(normalized)
147+
})
148+
149+
const modelValue = computed(() => control.control.value.data)
150+
151+
const minLength = computed(() =>
152+
resolveAutocompleteMinLength(
153+
control.control.value.uischema.options,
154+
control.appliedOptions.value,
155+
defaultMinLength,
156+
),
157+
)
158+
159+
const selectOptions = computed(() => {
160+
if (optionsList.value.length > 0) {
161+
return optionsList.value
162+
}
163+
164+
const staticOptions = getStaticOptions(control.control.value)
165+
if (staticOptions.length > 0) {
166+
return staticOptions
167+
}
168+
169+
return suggestions.value ?? []
170+
})
171+
172+
const clearPendingRequest = () => {
173+
if (abortController.value) {
174+
abortController.value.abort()
175+
abortController.value = null
176+
}
177+
}
178+
179+
const fetchOptions = async (search: string, uiOptions?: any) => {
180+
console.log('[autocomplete] fetchOptions search=', search)
181+
const apiConfig = extractAutocompleteApiConfig(
182+
uiOptions,
183+
control.appliedOptions.value,
184+
)
185+
186+
if (!apiConfig) {
187+
console.log('[autocomplete] no api config, fallback to static options')
188+
optionsList.value = []
189+
return
190+
}
191+
192+
const request = buildAutocompleteRequest(apiConfig, search)
193+
194+
clearPendingRequest()
195+
abortController.value = new AbortController()
196+
197+
try {
198+
console.log('[autocomplete] fetching URL:', request.url)
199+
const response = await fetch(request.url, {
200+
signal: abortController.value.signal,
201+
headers: request.headers,
202+
})
203+
204+
if (!response.ok) {
205+
console.warn('[autocomplete] HTTP error:', response.status)
206+
throw new Error(`HTTP ${response.status}`)
207+
}
208+
209+
const data = await response.json()
210+
const rawItems = apiConfig.itemsPath ? get(data, apiConfig.itemsPath) : data
211+
const items = isArray(rawItems) ? rawItems : []
212+
console.log('[autocomplete] items length:', items.length)
213+
optionsList.value = resolveFetchedOptions(items, apiConfig)
214+
console.log('[autocomplete] optionsList set:', optionsList.value)
215+
} catch (error) {
216+
console.warn('[autocomplete] API error:', error)
217+
optionsList.value = []
218+
}
219+
}
220+
221+
const onFilter = (
222+
value: string,
223+
update: (fn: () => void) => void,
224+
abort: () => void,
225+
) => {
226+
console.log('[autocomplete] onFilter value=', value)
227+
update(async () => {
228+
if (!value || value.length < minLength.value) {
229+
console.log('[autocomplete] onFilter below minLength, restoring static options')
230+
optionsList.value = getStaticOptions(control.control.value)
231+
return
232+
}
233+
234+
const uiOptions = control.control.value.uischema.options
235+
const apiConfig = extractAutocompleteApiConfig(
236+
uiOptions,
237+
control.appliedOptions.value,
238+
)
239+
240+
if (!apiConfig) {
241+
console.log('[autocomplete] onFilter using client-side filtering')
242+
const staticOptions = getStaticOptions(control.control.value)
243+
optionsList.value = filterOptionsBySearch(staticOptions, value)
244+
return
245+
}
246+
247+
console.log('[autocomplete] onFilter fetching remote options')
248+
await fetchOptions(value, uiOptions)
249+
})
250+
}
251+
252+
return {
253+
...control,
254+
adaptTarget,
255+
optionsList,
256+
selectOptions,
257+
suggestions,
258+
minLength,
259+
fetchOptions,
260+
onFilter,
261+
modelValue,
262+
}
263+
}

0 commit comments

Comments
 (0)