refactor(webapp): migrate ds-select to OcelotSelect (#9430)

This commit is contained in:
Ulf Gebhardt 2026-03-23 17:57:02 +01:00 committed by GitHub
parent d62abc524b
commit cadd0d0286
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 488 additions and 28 deletions

View File

@ -15,7 +15,7 @@ Phase 4: Tier 1 ██████████ 100% (OsButton, OsIcon, Os
Phase 4: Tier A → HTML ██████████ 100% (10 ds-* Wrapper → Plain HTML) ✅
Phase 4: Tier B ██████████ 100% (ds-chip→OsBadge✅, ds-tag→OsBadge✅, ds-grid✅, ds-number→OsNumber✅, ds-radio→HTML✅)
Phase 4: Tier B ██████████ 100% (Chip→OsBadge, Tag→OsBadge, Grid→HTML, Number→OsNumber, Radio→HTML, Table→HTML) ✅
Phase 4: Tier 2+ ████████░░ 60% (OsModal✅, ds-form entkoppelt✅, ds-input→OcelotInput✅) | Rest ausstehend (OsMenu, OsSelect, OsDropdown, OsAvatar)
Phase 4: Tier 2+ ████████░░ 70% (OsModal✅, ds-form entkoppelt✅, ds-input→OcelotInput✅, ds-select→OcelotSelect✅) | Rest ausstehend (OsMenu, OsDropdown, OsAvatar)
```
### Statistiken
@ -32,7 +32,8 @@ Phase 4: Tier 2+ ████████░░ 60% (OsModal✅, ds-form
| ✅ → OsModal | Modal (7 Nutzungen → OsModal, Focus-Trap, Scroll-Lock, A11y) |
| ✅ ds-input → OcelotInput | Input (23 Dateien → OcelotInput Webapp-Komponente, lokale Imports, formValidation-kompatibel) |
| ✅ ds-form entkoppelt | Form-Validierung → formValidation Mixin (async-validator), vuelidate entfernt |
| ⬜ → UI-Library | Menu, MenuItem, Select (3) — Tier 2-3 |
| ✅ ds-select → OcelotSelect | Select (3 Dateien → OcelotSelect Webapp-Komponente, lokale Imports, click-outside inline) |
| ⬜ → UI-Library | Menu, MenuItem (2) — Tier 3 |
| ⬜ Nicht in Webapp | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) |
### OsButton Migration (Phase 3) ✅
@ -80,7 +81,7 @@ Phase 4: Tier 2+ ████████░░ 60% (OsModal✅, ds-form
| 18 | InputError | ✅ → OcelotInput | In OcelotInput integriert |
| 19 | InputLabel | ✅ → OcelotInput | In OcelotInput integriert |
| 20 | Radio | ✅ → HTML | 1 Datei → native `<input type="radio">` + `<fieldset>` (ReportModal) |
| 21 | Select | ⬜ Tier 4 | 3 Dateien → OsSelect |
| 21 | Select | ✅ → OcelotSelect | 3 Dateien → OcelotSelect (Webapp-Komponente, click-outside inline) |
### Layout
| # | Komponente | Status | Notizen |
@ -370,7 +371,7 @@ Phase 4: Tier 2+ ████████░░ 60% (OsModal✅, ds-form
### Basis-Komponenten — UI-Library (ausstehend)
- Modal → OsModal ✅
- Input → OcelotInput (Webapp-Komponente) ✅ — langfristig → OsInput in packages/ui
- Select → OsSelect
- Select → OcelotSelect (Webapp-Komponente) ✅ — langfristig → OsSelect in packages/ui
- Avatar → OsAvatar (falls benötigt)
### Layout & Typography — → Plain HTML ✅ (Tier A)
@ -412,6 +413,7 @@ Phase 4: Tier 2+ ████████░░ 60% (OsModal✅, ds-form
| 2026-02-19 | Claude | **Tier A Migration** | 10 ds-* Vue-Wrapper → Plain HTML + CSS, _ds-compat.scss, ~450 Nutzungen in ~90 Dateien |
| 2026-02-19 | Claude | **Katalog konsolidiert** | Styleguide- und Webapp-Tabellen aktualisiert, veraltete Status korrigiert |
| 2026-03-23 | Claude | **ds-input → OcelotInput** | 23 Dateien migriert, Webapp-Komponente mit lokalen Imports (tree-shakeable), FormItem/Label/Error vereint |
| 2026-03-23 | Claude | **ds-select → OcelotSelect** | 3 Dateien migriert, Webapp-Komponente, DsSelect+inputMixin+multiinputMixin vereint, Form-Kopplung entfernt, DsChip→OsBadge, DsSpinner→OsSpinner, click-outside inline |
---
@ -450,11 +452,11 @@ Phase 4: Tier 2+ ████████░░ 60% (OsModal✅, ds-form
20. [x] ds-form → formValidation Mixin (async-validator), 18 Dateien migriert, vuelidate entfernt ✅
21. [x] ds-input → OcelotInput (23 Dateien, Webapp-Komponente mit lokalen Imports, FormItem/Label/Error vereint, formValidation-kompatibel) ✅
22. [ ] OsMenu / OsMenuItem (17 Dateien)
23. [ ] OsSelect (3 Dateien), OsTable (7 Dateien)
23. [x] ds-select → OcelotSelect (3 Dateien, Webapp-Komponente, click-outside inline, DsChip→OsBadge) ✅
---
**✅ Phase 0-3 abgeschlossen. Phase 4: Tier 1 + Tier A ✅, Tier B ✅ (Chip→OsBadge, Tag→OsBadge, Grid→HTML, Number→OsNumber, Radio→HTML, Table→HTML), Tier 2: OsModal ✅, ds-form entkoppelt ✅, ds-input → OcelotInput ✅, Rest ausstehend (OsMenu, OsSelect).**
**✅ Phase 0-3 abgeschlossen. Phase 4: Tier 1 + Tier A ✅, Tier B ✅ (Chip→OsBadge, Tag→OsBadge, Grid→HTML, Number→OsNumber, Radio→HTML, Table→HTML), Tier 2: OsModal ✅, ds-form entkoppelt ✅, ds-input → OcelotInput ✅, ds-select → OcelotSelect ✅, Rest ausstehend (OsMenu).**
---

View File

@ -81,10 +81,10 @@ Phase 0: ██████████ 100% (6/6 Aufgaben) ✅
Phase 1: ██████████ 100% (6/6 Aufgaben) ✅
Phase 2: ██████████ 100% (26/26 Aufgaben) ✅
Phase 3: ██████████ 100% (24/24 Aufgaben) ✅ - Webapp-Integration komplett
Phase 4: ████████░░ 74% (20/27 Aufgaben) - Tier 1 ✅, Tier A ✅, Infra ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅, OsNumber ✅, OsModal ✅, ds-radio→HTML ✅ | Tier B ✅, OcelotInput ✅, Tier 2-3 Rest ausstehend
Phase 4: ████████░░ 78% (21/27 Aufgaben) - Tier 1 ✅, Tier A ✅, Infra ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅, OsNumber ✅, OsModal ✅, ds-radio→HTML ✅ | Tier B ✅, OcelotInput ✅, OcelotSelect ✅, Tier 2-3 Rest ausstehend
Phase 5: ░░░░░░░░░░ 0% (0/7 Aufgaben)
───────────────────────────────────────
Gesamt: ████████░░ 85% (82/96 Aufgaben)
Gesamt: ████████░░ 86% (83/96 Aufgaben)
```
### Katalogisierung (Details in KATALOG.md)
@ -289,7 +289,7 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
- [x] 0 Tier-A `ds-*` Komponenten-Tags verbleibend
**Verbleibende ds-* Komponenten (6 Typen):**
- Tier C (→ UI-Library): ds-modal (7→✅ OsModal), ds-input (23→✅ OcelotInput), ds-menu/ds-menu-item (17), ds-select (3)
- Tier C (→ UI-Library): ds-modal (7→✅ OsModal), ds-input (23→✅ OcelotInput), ds-select (3→✅ OcelotSelect), ds-menu/ds-menu-item (17)
- ✅ ds-form (18 Dateien) → formValidation Mixin (async-validator), vuelidate entfernt
**Zuvor abgeschlossen (Session 26 - CodeRabbit Review Fixes):**
@ -419,7 +419,7 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
- [x] ds-form → formValidation Mixin (async-validator), 18 Dateien migriert, vuelidate entfernt ✅
- [x] ds-input → OcelotInput (23 Dateien, Webapp-Komponente mit lokalen Imports, formValidation-kompatibel) ✅
- [ ] ds-menu / ds-menu-item → OsMenu / OsMenuItem
- [ ] ds-select → OsSelect
- [x] ds-select → OcelotSelect (3 Dateien, Webapp-Komponente mit lokalen Imports, click-outside inline) ✅
- [ ] Browser-Fehler untersuchen: `TypeError: Cannot read properties of undefined (reading 'heartO')` (ocelotIcons undefined im Browser trotz korrekter Webpack-Aliase)
**Manuelle Setup-Aufgaben (außerhalb Code):**
@ -701,7 +701,7 @@ Jeder migrierte Button muss manuell geprüft werden: Normal, Hover, Focus, Activ
- [ ] OsMenuItem (Basis: DsMenuItem, 6 Dateien)
**Tier 4: Spezial-Komponenten**
- [ ] OsSelect (3 Dateien)
- [x] ds-select → OcelotSelect (3 Dateien, Webapp-Komponente, click-outside inline, DsChip→OsBadge, DsSpinner→OsSpinner) ✅
- [x] ds-table (7 Dateien) → Plain HTML `<table>` + CSS-Klassen ✅ (kein OsTable nötig)
- [x] ds-form → formValidation Mixin (async-validator), vuelidate entfernt ✅
@ -1853,6 +1853,8 @@ Bei der Migration werden:
| 2026-03-14 | **18 Formulare migriert** | CommentForm, ContributionForm, EnterNonce, GroupForm, Password/Change, PasswordReset (2), Registration (5), Signup, MySomethingList, donations, admin/users, settings (3) |
| 2026-03-20 | **formValidation Fix** | `handleInput()` vor `$validateForm()` aufrufen (Reihenfolge-Bug: handleInput überschrieb handleInputValid bei synchronem async-validator Callback) |
| 2026-03-23 | **ds-input → OcelotInput** | Neue Webapp-Komponente `OcelotInput.vue`: vereint DsInput + FormItem + InputLabel + InputError in einer Datei. 23 Vue-Dateien migriert mit lokalen Imports (tree-shakeable). formValidation Mixin voll kompatibel. dot-prop Abhängigkeit durch inline `getNestedValue()` ersetzt. 28 Test-Suites, 210 Tests ✅, 7 Snapshots aktualisiert. |
| 2026-03-23 | **OcelotInput: ds-icon → os-icon** | DsIcon durch OsIcon + resolveIcon() ersetzt. at.svg, envelope.svg, paperclip.svg zu Ocelot-Icons hinzugefügt. Ocelot-Icons Visual Snapshot aktualisiert. |
| 2026-03-23 | **ds-select → OcelotSelect** | Neue Webapp-Komponente `OcelotSelect.vue`: vereint DsSelect + inputMixin + multiinputMixin (~420 Zeilen). Form-Validation entfernt (von keinem Consumer genutzt). DsChip→OsBadge, DsSpinner→OsSpinner, DsIcon→OsIcon. vue-click-outside durch inline document.addEventListener ersetzt. 3 Dateien migriert, 16 Tests ✅. |
---
@ -1875,7 +1877,8 @@ Bei der Migration werden:
| ✅ → UI-Library | Chip, Tag → OsBadge (2), Number → OsNumber (1) — Tier B |
| ✅ ds-form entkoppelt | Form-Validierung → formValidation Mixin (async-validator), vuelidate entfernt |
| ✅ ds-input → OcelotInput | Webapp-Komponente (23 Dateien), lokale Imports, FormItem/InputLabel/InputError vereint |
| ⬜ → UI-Library | Menu, MenuItem, Select (3) — Tier 2-3 |
| ✅ ds-select → OcelotSelect | Webapp-Komponente (3 Dateien), lokale Imports, click-outside inline, DsChip→OsBadge |
| ⬜ → UI-Library | Menu, MenuItem (2) — Tier 3 |
| ⬜ Nicht genutzt | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) |
---

View File

@ -0,0 +1,450 @@
<template>
<div class="ds-form-item" :class="stateClasses">
<label class="ds-input-label" v-show="!!label" :for="id">
{{ label }}
</label>
<div
class="ds-select-wrap"
:class="[isOpen && 'ds-select-is-open']"
:tabindex="searchable ? -1 : tabindex"
@keydown.tab="closeAndBlur"
@keydown.self.down.prevent="pointerNext"
@keydown.self.up.prevent="pointerPrev"
@keypress.enter.prevent.stop="handleEnter"
@keyup.esc="close"
>
<div v-if="resolvedIcon" class="ds-select-icon">
<os-icon :icon="resolvedIcon" />
</div>
<div
class="ds-select"
@click="openAndFocus"
:class="[
resolvedIcon && 'ds-select-has-icon',
resolvedIconRight && 'ds-select-has-icon-right',
multiple && 'ds-select-multiple',
]"
>
<div v-if="multiple" class="ds-selected-options">
<div
class="ds-selected-option"
v-for="(val, index) in innerValue"
:key="val[labelProp] || val"
>
<slot name="optionitem" :value="val">
<os-badge removable @remove="deselectOption(index)" variant="primary" :size="size">
{{ val[labelProp] || val }}
</os-badge>
</slot>
</div>
<input
ref="search"
class="ds-select-search"
autocomplete="off"
:id="id"
:name="name ? name : model"
:autofocus="autofocus"
:placeholder="placeholder"
:tabindex="tabindex"
:disabled="disabled"
v-model="searchString"
@focus="openAndFocus"
@keydown.tab="closeAndBlur"
@keydown.delete.stop="deselectLastOption"
@keydown.down.prevent="handleKeyDown"
@keydown.up.prevent="handleKeyUp"
@keypress.enter.prevent.stop="handleEnter"
@keyup.esc="close"
/>
</div>
<div v-else class="ds-select-value">
<slot v-if="innerValue" name="value" :value="innerValue">
{{ innerValue[labelProp] || innerValue }}
</slot>
<div v-else-if="placeholder" class="ds-select-placeholder">
{{ placeholder }}
</div>
</div>
<input
v-if="!multiple"
ref="search"
class="ds-select-search"
autocomplete="off"
:id="id"
:name="name ? name : model"
:autofocus="autofocus"
:placeholder="placeholder"
:tabindex="tabindex"
:disabled="disabled"
v-model="searchString"
@focus="openAndFocus"
@keydown.tab="closeAndBlur"
@keydown.delete.stop="deselectLastOption"
@keydown.down.prevent="handleKeyDown"
@keydown.up.prevent="handleKeyUp"
@keypress.enter.prevent.stop="handleEnter"
@keyup.esc="close"
/>
</div>
<div class="ds-select-dropdown">
<div class="ds-select-dropdown-message" v-if="!options || !options.length">
{{ noOptionsAvailable }}
</div>
<div class="ds-select-dropdown-message" v-else-if="!filteredOptions.length">
{{ noOptionsFound }} "{{ searchString }}"
</div>
<ul class="ds-select-options" ref="options" v-else>
<li
class="ds-select-option"
:class="[
isSelected(option) && 'ds-select-option-is-selected',
pointer === index && 'ds-select-option-hover',
]"
v-for="(option, index) in filteredOptions"
@click="handleSelect(option)"
@mouseover="setPointer(index)"
:key="option[labelProp] || option"
>
<slot name="option" :option="option">
{{ option[labelProp] || option }}
</slot>
</li>
</ul>
</div>
<div v-if="resolvedIconRight" class="ocelot-select-icon-right">
<os-icon :icon="resolvedIconRight" />
</div>
</div>
</div>
</template>
<script>
import { OsIcon, OsBadge } from '@ocelot-social/ui'
import { resolveIcon } from '~/utils/iconRegistry'
export default {
name: 'OcelotSelect',
components: { OsIcon, OsBadge },
props: {
value: {
type: [String, Object, Number, Array],
default: null,
},
model: {
type: String,
default: null,
},
name: {
type: String,
default: null,
},
label: {
type: String,
default: null,
},
id: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'base',
validator: (value) => /^(small|base|large)$/.test(value),
},
tabindex: {
type: Number,
default: 0,
},
multiple: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
autofocus: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: null,
},
iconRight: {
type: String,
default: 'angle-down',
},
options: {
type: Array,
default: () => [],
},
labelProp: {
type: String,
default: 'label',
},
searchable: {
type: Boolean,
default: true,
},
autoResetSearch: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: false,
},
filter: {
type: Function,
default: (option, searchString = '', labelProp) => {
const value = String(option[labelProp] || option)
const searchParts = typeof searchString === 'string' ? searchString.split(' ') : []
return searchParts.every((part) => {
if (!part) return true
return value.toLowerCase().includes(part.toLowerCase())
})
},
},
noOptionsAvailable: {
type: String,
default: 'No options available.',
},
noOptionsFound: {
type: String,
default: 'No options found for:',
},
},
data() {
return {
innerValue: null,
error: null,
focus: false,
searchString: '',
pointer: 0,
isOpen: false,
hadKeyboardInput: null,
}
},
computed: {
resolvedIcon() {
return resolveIcon(this.icon)
},
resolvedIconRight() {
return resolveIcon(this.iconRight)
},
isInteractionBlocked() {
return this.disabled || this.readonly || this.loading
},
stateClasses() {
return [
this.size && `ds-input-size-${this.size}`,
this.disabled && 'ds-input-is-disabled',
this.readonly && 'ds-input-is-readonly',
this.error && 'ds-input-has-error',
this.focus && 'ds-input-has-focus',
]
},
filteredOptions() {
if (!this.searchString) return this.options
return this.options.filter((option) => this.filter(option, this.searchString, this.labelProp))
},
pointerMax() {
return this.filteredOptions.length - 1
},
},
watch: {
value: {
handler(value) {
this.innerValue = value
},
deep: true,
immediate: true,
},
pointerMax(max) {
if (max < this.pointer) {
this.$nextTick(() => {
this.pointer = max
})
}
},
searchString() {
this.setPointer(-1)
},
},
mounted() {
this._clickOutsideHandler = (e) => {
if (!this.$el.contains(e.target)) {
this.closeAndBlur()
}
}
document.addEventListener('click', this._clickOutsideHandler, true)
},
beforeDestroy() {
document.removeEventListener('click', this._clickOutsideHandler, true)
clearTimeout(this.hadKeyboardInput)
},
methods: {
// --- Input / Value ---
input(value) {
this.innerValue = value
this.$emit('input', value)
},
// --- Selection ---
selectOption(option) {
if (this.multiple) {
this.selectMultiOption(option)
} else {
this.input(option)
}
},
selectMultiOption(value) {
if (!this.innerValue) return this.input([value])
const index = this.innerValue.indexOf(value)
if (index < 0) return this.input([...this.innerValue, value])
this.deselectOption(index)
},
deselectOption(index) {
const newArray = [...this.innerValue]
newArray.splice(index, 1)
this.input(newArray)
},
deselectLastOption() {
if (this.multiple && this.innerValue && this.innerValue.length && !this.searchString.length) {
this.deselectOption(this.innerValue.length - 1)
}
},
isSelected(option) {
if (!this.innerValue) return false
if (this.multiple) return this.innerValue.includes(option)
return this.innerValue === option
},
// --- Select interaction ---
handleSelect(option) {
if (this.isInteractionBlocked) return
if (this.pointerMax < 0) return
this.selectOption(option)
if (this.autoResetSearch || this.multiple) this.resetSearch()
if (this.multiple) {
this.$refs.search.focus()
this.handleFocus()
} else {
this.close()
}
},
resetSearch() {
this.searchString = ''
},
openAndFocus() {
if (this.isInteractionBlocked) return
this.open()
if (this.autoResetSearch) this.resetSearch()
if (!this.focus || this.multiple) {
this.$refs.search.focus()
this.handleFocus()
}
},
open() {
if (this.autoResetSearch || this.multiple) this.resetSearch()
this.isOpen = true
},
close() {
this.isOpen = false
},
closeAndBlur() {
this.close()
if (this.$refs.search) this.$refs.search.blur()
this.handleBlur()
},
handleFocus() {
this.focus = true
},
handleBlur() {
this.focus = false
},
// --- Keyboard navigation ---
handleEnter(e) {
if (this.isInteractionBlocked) return
if (this.pointer >= 0) {
this.selectPointerOption()
} else {
this.setPointer(-1)
this.$emit('enter', e)
}
},
handleKeyUp() {
if (this.isInteractionBlocked) return
if (!this.isOpen) {
this.open()
return
}
this.pointerPrev()
},
handleKeyDown() {
if (this.isInteractionBlocked) return
if (!this.isOpen) {
this.open()
return
}
this.pointerNext()
},
setPointer(index) {
if (!this.hadKeyboardInput) this.pointer = index
},
pointerPrev() {
if (this.pointer <= 0) {
this.pointer = this.pointerMax
} else {
this.pointer--
}
this.scrollToHighlighted()
},
pointerNext() {
if (this.pointer >= this.pointerMax) {
this.pointer = 0
} else {
this.pointer++
}
this.scrollToHighlighted()
},
scrollToHighlighted() {
clearTimeout(this.hadKeyboardInput)
if (!this.$refs.options || !this.$refs.options.children.length || this.pointerMax <= -1)
return
this.hadKeyboardInput = setTimeout(() => {
this.hadKeyboardInput = null
}, 250)
this.$refs.options.children[this.pointer].scrollIntoView({ block: 'nearest' })
},
selectPointerOption() {
this.handleSelect(this.filteredOptions[this.pointer])
},
},
}
</script>
<style lang="scss">
/* Styles inherited from global styleguide CSS (ds-select, ds-form-item classes).
* Once ds-select is fully removed from the styleguide, move the styles here. */
.ocelot-select-icon-right {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -3,7 +3,7 @@
<label class="ds-input-label">
{{ `${$t('settings.data.labelCity')}` + locationNameLabelAddOnOldName }}
</label>
<ds-select
<ocelot-select
id="city"
v-model="currentValue"
:options="cities"
@ -14,13 +14,15 @@
@input.native="handleCityInput"
/>
<os-button
v-if="locationName !== '' && canBeCleared"
v-if="(locationName !== '' && canBeCleared) || loadingGeo"
data-test="clear-location-button"
variant="primary"
appearance="ghost"
size="sm"
circle
:loading="loadingGeo"
:aria-label="$t('actions.clear')"
style="right: -94%; top: -48px"
style="position: relative; float: right; top: -48px; right: 4px"
@click="clearLocationName"
>
<template #icon><os-icon :icon="icons.close" /></template>
@ -30,14 +32,13 @@
<script>
import { OsButton, OsIcon } from '@ocelot-social/ui'
import OcelotSelect from '~/components/OcelotSelect/OcelotSelect.vue'
import { iconRegistry } from '~/utils/iconRegistry'
import { queryLocations } from '~/graphql/location'
let timeout
export default {
name: 'LocationSelect',
components: { OsButton, OsIcon },
components: { OsButton, OsIcon, OcelotSelect },
props: {
value: {
type: [String, Object],
@ -62,6 +63,7 @@ export default {
return {
currentValue: this.value,
loadingGeo: false,
debounceTimeout: null,
cities: [],
}
},
@ -99,11 +101,9 @@ export default {
},
methods: {
handleCityInput(event) {
clearTimeout(timeout)
timeout = setTimeout(
() => this.requestGeoData(event.target ? event.target.value.trim() : ''),
500,
)
const value = event.target ? event.target.value.trim() : ''
clearTimeout(this.debounceTimeout)
this.debounceTimeout = setTimeout(() => this.requestGeoData(value), 500)
},
processLocationsResult(places) {
if (!places.length) {

View File

@ -1,6 +1,6 @@
<template>
<div class="searchable-input" aria-label="search" role="search">
<ds-select
<ocelot-select
ref="select"
type="search"
icon="search"
@ -48,13 +48,14 @@
<hc-hashtag :id="option.id" />
</p>
</template>
</ds-select>
</ocelot-select>
<os-button
v-if="isActive"
v-if="isActive || loading"
variant="primary"
appearance="ghost"
circle
size="sm"
:loading="loading"
:aria-label="$t('actions.clear')"
@click="clear"
>
@ -67,6 +68,7 @@
<script>
import { OsButton, OsIcon } from '@ocelot-social/ui'
import OcelotSelect from '~/components/OcelotSelect/OcelotSelect.vue'
import { iconRegistry } from '~/utils/iconRegistry'
import { isEmpty } from 'lodash'
import SearchHeading from '~/components/generic/SearchHeading/SearchHeading.vue'
@ -80,6 +82,7 @@ export default {
components: {
OsButton,
OsIcon,
OcelotSelect,
SearchHeading,
SearchGroup,
SearchPost,

View File

@ -1,5 +1,5 @@
<template>
<ds-select
<ocelot-select
class="select-user-search"
type="search"
icon="search"
@ -26,18 +26,20 @@
<user-teaser :user="option" :showPopover="false" :linkToProfile="false" />
</p>
</template>
</ds-select>
</ocelot-select>
</template>
<script>
import { isEmpty } from 'lodash'
import { searchUsers } from '~/graphql/Search.js'
import UserTeaser from '~/components/UserTeaser/UserTeaser.vue'
import OcelotSelect from '~/components/OcelotSelect/OcelotSelect.vue'
export default {
name: 'SelectUserSearch',
components: {
UserTeaser,
OcelotSelect,
},
props: {
id: {