From caa6f67252e6bd6f6e8496c267993d05e9690b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Justinas=20Kavoli=C5=ABnas?= Date: Fri, 6 Nov 2026 20:48:27 +0200 Subject: [PATCH 1/9] Duty picker: search duty and institution words together, show more, shared rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Duty search_text now includes the institution name and padalinys shortname, so "atstovas matematikos" / "pirmininkas MIF" match (Typesense matches all tokens inside one field; duty and institution names were separate fields). - DutySearchField renders the shared SearchHitRow (highlights, current holders, tenant and inactive badges) and offers "Rodyti daugiau" with keyboard support. SearchHitRow gets an optional trailing="none" for pickers. - AssignDutyUserSheet: "use original duty title" moved out of Papildomi nustatymai. - Changelog and Pareigybės / Nariai guide pages updated. Deploy: reindex duties (scout:import "App\Models\Duty") for the combined search. Written by Codex. Claude review: plan, diff review, test runs, fixed one assertion that relied on placeholder replacement the test i18n mock doesn't do. Co-Authored-By: Claude Opus 5.5 --- app/Services/Typesense/SearchText.php | 8 + docs/changelog/v3.md | 3 + docs/en/changelog/v3.md | 3 + docs/organizacija/nariai.md | 2 +- docs/organizacija/pareigybes.md | 30 +++- lang/en.json | 1 + lang/lt.json | 1 + .../AdminSearch/Components/SearchHitRow.vue | 9 +- .../__tests__/SearchHitRow.component.test.ts | 11 ++ .../Admin/Occupancy/AssignDutyUserSheet.vue | 35 ++-- .../Admin/Occupancy/DutySearchField.vue | 59 +++++-- .../AssignDutyUserSheet.component.test.ts | 14 ++ .../DutySearchField.component.test.ts | 160 ++++++++++++++++++ tests/Feature/Search/SearchExperienceTest.php | 16 ++ 14 files changed, 308 insertions(+), 44 deletions(-) create mode 100644 resources/js/Features/Admin/Occupancy/__tests__/DutySearchField.component.test.ts diff --git a/app/Services/Typesense/SearchText.php b/app/Services/Typesense/SearchText.php index ee0c1db5b..2a2806d84 100644 --- a/app/Services/Typesense/SearchText.php +++ b/app/Services/Typesense/SearchText.php @@ -4,6 +4,7 @@ use App\Models\Calendar; use App\Models\Content; +use App\Models\Duty; use App\Models\News; use App\Models\Page; use App\Services\ContentService; @@ -21,6 +22,9 @@ public static function plain(?string $value): string public static function forModel(Model $model): array { $result = []; + if ($model instanceof Duty) { + $model->loadMissing('institution.tenant'); + } if ($model instanceof News || $model instanceof Page) { $model->loadMissing('content.parts', 'tags'); $content = $model->getRelation('content'); @@ -66,6 +70,10 @@ public static function forModel(Model $model): array $parts[] = $tag->getTranslations('name')[$locale] ?? ''; } } + if ($model instanceof Duty) { + $parts[] = $model->institution?->getTranslations('name')[$locale] ?? ''; + $parts[] = $model->institution?->tenant?->shortname ?? ''; + } $result['search_text_'.$locale] = self::plain(implode(' ', $parts)); } diff --git a/docs/changelog/v3.md b/docs/changelog/v3.md index 912e1b7a6..9137b261e 100644 --- a/docs/changelog/v3.md +++ b/docs/changelog/v3.md @@ -34,6 +34,9 @@ Datos platformoje ir vusa.lt rodomos vienodai ir visada Vilniaus laiku. Gide: [D ### Kiti pakeitimai ir pataisymai +- 🔧 **Pareigybės paieška** – randa pareigybes pagal kartu įrašytus pareigybės ir institucijos ar padalinio žodžius, pvz., „atstovas matematikos“ arba „pirmininkas MIF“. Gide: [Pareigybės pasirinkimas](/organizacija/pareigybes#priskirti-nari) +- ✨ **„Rodyti daugiau“** – pareigybės pasirinkimo sąrašą galima papildyti tame pačiame lange. Sutapę žodžiai paryškinami, rodomi dabartiniai pareigas einantys nariai. Gide: [Pareigybės pasirinkimas](/organizacija/pareigybes#priskirti-nari) +- ✨ **Originalus pareigybės pavadinimas** – parinktis „Viešame puslapyje naudoti originalų pareigybės pavadinimą“ matoma iškart po pareigybe, neatveriant „Papildomi nustatymai“. Gide: [Nario priskyrimas](/organizacija/pareigybes#priskirti-nari) - 🔧 **Registracijų eksportas** – narių ir studentų atstovų registracijų eksporte pateikiamos tik tos registracijos, kurias naudotojas gali matyti formos puslapyje. Gide: [Atsakymų eksportavimas](/organizacija/formos-ir-registracijos#eksportas) - 🔧 **Dokumentų tikrinimas** – sėkmingai tikrinami dokumentai ir toliau automatiškai atnaujinami, o pakartotinai nepavykę patikrinimai neužima kitų dokumentų tikrinimo vietų. Gide: [Dokumentai](/visak/dokumentai#pranesimai) - 🔧 **Paieška** – jei per trumpą laiką ieškota per daug kartų, paieška vėl veikia po kelių sekundžių, nebereikia perkrauti puslapio. Sąrašai atveriami su nuorodoje pasirinkta paieška ir filtrais nuo pirmojo rezultatų įkėlimo. Paieškos lange veiksmas „Paklausti, ar vyko posėdžiai“ atveria užklausos kūrimą, o ne pradinį veiksmų langą. Gide: [Rasti skiltį ar įrašą](/pagrindai/platforma#bendra-paieska) diff --git a/docs/en/changelog/v3.md b/docs/en/changelog/v3.md index 0b67b9c31..e589e4b4e 100644 --- a/docs/en/changelog/v3.md +++ b/docs/en/changelog/v3.md @@ -34,6 +34,9 @@ Dates on the platform and vusa.lt now read the same everywhere and always in Vil ### Other changes and fixes +- 🔧 **Duty search** – finds duties using duty and institution or unit words together, such as “atstovas matematikos” or “pirmininkas MIF”. Guide: [Choosing a duty](/organizacija/pareigybes#priskirti-nari) +- ✨ **“Show more”** – adds more matches to the duty picker in the same window. Matching words are highlighted and current duty holders are shown. Guide: [Choosing a duty](/organizacija/pareigybes#priskirti-nari) +- ✨ **Original duty title** – the option to use the original duty title on the public website is visible directly below the duty, without opening “Additional settings”. Guide: [Assigning a member](/organizacija/pareigybes#priskirti-nari) - 🔧 **Registration exports** – member and student representative registration exports now include only the registrations the user can see on the form page. Guide: [Exporting responses](/organizacija/formos-ir-registracijos#eksportas) - 🔧 **Document checks** – documents with successful checks continue to refresh automatically, and repeatedly failed checks no longer take up other documents’ refresh slots. Guide: [Documents](/visak/dokumentai#pranesimai) - 🔧 **Search** – after too many searches in a short time, search works again within a few seconds instead of needing a page reload. Lists apply the search and filters from the link on their first load. In the search window, the „Ask whether meetings happened“ action opens the request itself instead of the starting action window. Guide: [Finding a section or record](/pagrindai/platforma#bendra-paieska) diff --git a/docs/organizacija/nariai.md b/docs/organizacija/nariai.md index 1b3d43446..b49ec74d1 100644 --- a/docs/organizacija/nariai.md +++ b/docs/organizacija/nariai.md @@ -175,7 +175,7 @@ Profilis suskirstytas į skirtukus: - *Dabartinės pareigos* – aktyvūs pareigybių laikotarpiai, institucija, kadencijos pradžia ir pabaiga. - *Būsimos pareigos* – pareigos, kurios prasidės ateityje. - *Buvusios pareigos* – visa istorinė asmens veikla VU SA. - - Mygtukas **Pridėti pareigybę** atidaro šoninį pareigybės priskyrimo langą (pasirenkama pareigybė, kadencijos datos ir studijų programa). + - Mygtukas **Pridėti pareigybę** atidaro [pareigybės pasirinkimo langą](/organizacija/pareigybes#priskirti-nari), kuriame pasirenkama pareigybė ir pareigų datos, o kuratoriams – ir studijų programa. 2. **Prieiga** (matoma superadministratoriui arba kai narys pats yra superadministratorius): - Jungiklis **Super administratorius** suteikia arba atima visą prieigą prie platformos. Jį perjungti gali tik Superadministratorius; prieš išsaugant prašoma patvirtinti. - Kitas teises narys gauna per savo pareigybių atsakomybes. Anksčiau tiesiogiai paskyrai suteiktos atsakomybės matomos ir atimamos [atsakomybės skirtuke „Kam suteikta“](/sistema/roles-ir-leidimai#atsakomybes-suteikimas). diff --git a/docs/organizacija/pareigybes.md b/docs/organizacija/pareigybes.md index 81ebd8e1d..e2423390d 100644 --- a/docs/organizacija/pareigybes.md +++ b/docs/organizacija/pareigybes.md @@ -1,7 +1,7 @@ --- doc_status: reviewed title: Pareigybės -last_reviewed: 2026-11-01 +last_reviewed: 2026-11-06 tests: - tests/Browser/CollectionTrashRefreshTest.php - resources/js/Components/AdminForms/__tests__/DutyForm.component.test.ts @@ -51,6 +51,8 @@ tests: - resources/js/Features/Admin/Responsibilities/__tests__/DutyResponsibilitiesSection.component.test.ts - resources/js/Features/Admin/Occupancy/__tests__/occupancy.test.ts - resources/js/Features/Admin/Occupancy/__tests__/AssignDutyUserSheet.component.test.ts + - resources/js/Features/Admin/Occupancy/__tests__/DutySearchField.component.test.ts + - tests/Feature/Search/SearchExperienceTest.php - resources/js/Features/Admin/Occupancy/__tests__/MemberSearchField.component.test.ts - resources/js/Components/Duties/__tests__/DutyPreview.component.test.ts - resources/js/Composables/__tests__/useDuplicateDutyCheck.test.ts @@ -364,18 +366,34 @@ Užbaigimo patvirtinimas aiškiai paaiškina, kad šiandien pareigos dar galioja ### Nario priskyrimas ir šalinimas {#priskirti-nari} Paspaudus **Priskirti** pareigybės puslapyje: -1. Pasirenkamas narys. Paieškoje pirmiausia rodomi dabartiniai nariai, o buvę pažymimi „Buvęs narys · metai“. +1. Po pareigybės pavadinimu galima pažymėti **Viešame puslapyje naudoti originalų pareigybės pavadinimą**. + Ši parinktis matoma iškart, neatveriant papildomų nustatymų. +2. Pasirenkamas narys. Paieškoje pirmiausia rodomi dabartiniai nariai, o buvę pažymimi „Buvęs narys · metai“. Jei žmogaus sistemoje dar nėra, jį galima sukurti čia pat – užtenka vardo ir pavardės, o el. paštas nebūtinas (aktualu buvusiems nariams; be el. pašto jie tiesiog negauna laiškų). Sukurti naują narį gali tie, kurie turi teisę kurti narius. -2. Pasirenkama **kadencija**. Pagal nutylėjimą – dabartinė: pradžia šiandien, pabaiga – kadencijos +3. Pasirenkama **kadencija**. Pagal nutylėjimą – dabartinė: pradžia šiandien, pabaiga – kadencijos pabaiga. Pasirinkus ankstesnę kadenciją, datos užpildomos visam jos laikotarpiui. Kadencijos imamos iš institucijos [kadencijų](/sistema/nustatymai#kadencijos), o jei institucija savų neturi – iš bendrųjų. -3. Jei reikia, datos pataisomos ranka – tada pasirinkimas tampa „Kitos datos“. -4. **Papildomi nustatymai** (suskleisti): papildomas el. paštas, nuotrauka, originalus pareigybės - pavadinimas ir **Papildomas aprašymas**, rodomas viešame kontaktų sąraše vietoj pareigybės aprašymo. +4. Jei reikia, datos pataisomos ranka – tada pasirinkimas tampa „Kitos datos“. +5. **Papildomi nustatymai** (suskleisti): papildomas el. paštas, nuotrauka ir **Papildomas aprašymas**, + rodomas viešame kontaktų sąraše vietoj pareigybės aprašymo. Kuratorių pareigybėms čia rodoma ir studijų programa bei jos pastaba. +Nario profilyje **Pridėti pareigybę** atveria tą patį langą su jau pasirinktu nariu. +Pareigybės paieškoje galima kartu įrašyti pareigybės ir institucijos ar padalinio žodžius, +pvz., „atstovas matematikos“ arba „pirmininkas MIF“. Sutapę žodžiai paryškinami, o prie pareigybės +rodomi ją šiuo metu einantys nariai. Jei rezultatų daugiau, paskutinė eilutė **Rodyti daugiau** +parodo, kiek jau rodoma ir kiek iš viso rasta; ją pasirinkus sąrašas papildomas. + + + +Paieška randa pareigybes pagal kartu įrašytus pareigybės ir institucijos ar padalinio žodžius, +o daugiau rezultatų galima parodyti tame pačiame sąraše. Originalaus pavadinimo parinktis matoma +iškart po pareigybe. + + + Jei per pasirinktą laikotarpį tas pats žmogus jau eina šias pareigas, jo pasirinkti negalima. Pasibaigęs laikotarpis neužima vietos, todėl jam padalinio kvota netaikoma. diff --git a/lang/en.json b/lang/en.json index 582eb53b6..1dafaf28b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,4 +1,5 @@ { + "Rodyti daugiau (:shown iš :total)": "Show more (:shown of :total)", "Daugiausiai klausimų": "Most items", "Mažiausiai klausimų": "Fewest items", "Darbotvarkės klausimų nėra": "No agenda items", diff --git a/lang/lt.json b/lang/lt.json index c0651996a..01ad7f6a5 100644 --- a/lang/lt.json +++ b/lang/lt.json @@ -1,4 +1,5 @@ { + "Rodyti daugiau (:shown iš :total)": "Rodyti daugiau (:shown iš :total)", "Daugiausiai klausimų": "Daugiausiai klausimų", "Mažiausiai klausimų": "Mažiausiai klausimų", "Nėra darbotvarkės klausimų": "Nėra darbotvarkės klausimų", diff --git a/resources/js/Features/Admin/AdminSearch/Components/SearchHitRow.vue b/resources/js/Features/Admin/AdminSearch/Components/SearchHitRow.vue index 9c5824164..03b7be8e3 100644 --- a/resources/js/Features/Admin/AdminSearch/Components/SearchHitRow.vue +++ b/resources/js/Features/Admin/AdminSearch/Components/SearchHitRow.vue @@ -82,7 +82,7 @@ +
+ +
+ +

+ {{ $t('Matoma vusa.lt') }} +

+
+
+ -
- -
- -

- {{ $t('Matoma vusa.lt') }} -

-
-
-

{{ $t('Papildomas aprašymas') }} @@ -572,7 +572,6 @@ const descriptionLocale = ref<(typeof LOCALES)[number]>('lt'); const hasExtras = () => hasDescription.value || form.additional_email.trim() !== '' || !!form.photo_media - || form.use_original_duty_name || !!form.study_program_id || hasText(form.study_program_note); diff --git a/resources/js/Features/Admin/Occupancy/DutySearchField.vue b/resources/js/Features/Admin/Occupancy/DutySearchField.vue index 890964e66..c448ec490 100644 --- a/resources/js/Features/Admin/Occupancy/DutySearchField.vue +++ b/resources/js/Features/Admin/Occupancy/DutySearchField.vue @@ -73,20 +73,25 @@ @click="choose(hit)" @keydown.enter.prevent="choose(hit)" > -

-

- -

-

- {{ hit.institutionName }} -

-
- - {{ hit.tenantShortname }} - + + +
  • +
  • {{ isSearching ? $t('Ieškoma…') : $t('Pareigybių nerasta') }} @@ -110,7 +115,9 @@ import EntityTypeMark from '@/Components/EntityTypeMark.vue'; import { Button } from '@/Components/ui/button'; import { Input } from '@/Components/ui/input'; import { Label } from '@/Components/ui/label'; +import SearchHitRow from '@/Features/Admin/AdminSearch/Components/SearchHitRow.vue'; import { useAdminCollectionSearch } from '@/Features/Admin/AdminSearch/Composables/useAdminCollectionSearch'; +import { normalizeHit, type MapperContext } from '@/Features/Admin/AdminSearch/Utils/searchHitMappers'; import { ModelEnum } from '@/Types/enums'; /** What the sheet needs from a `duties` search document. */ @@ -168,6 +175,12 @@ const query = ref(''); const showList = computed(() => query.value.trim().length >= 2); const isSearching = computed(() => collection.isSearching.value); +const isLoadingMore = computed(() => collection.status.value === 'loading-more'); +const mapperContext = computed(() => ({ + ownTenantIds: collection.adminSearch.getCollectionTenantIds('duties'), + isSuperAdmin: collection.adminSearch.isSuperAdmin.value, +})); +const rows = computed(() => collection.results.value.map(doc => normalizeHit('duties', doc, mapperContext.value))); const activeOptionId = computed(() => (activeIndex.value >= 0 ? optionId(activeIndex.value) : undefined)); const hits = computed(() => (showList.value @@ -181,6 +194,7 @@ const hits = computed(() => (showList.value isActive: doc.is_active !== false, })) : [])); +const optionCount = computed(() => hits.value.length + (collection.hasMoreResults.value ? 1 : 0)); watch(query, (value) => { activeIndex.value = -1; @@ -191,11 +205,13 @@ watch(query, (value) => { }); const move = (step: 1 | -1) => { - if (hits.value.length === 0) { + if (optionCount.value === 0) { return; } - activeIndex.value = (activeIndex.value + step + hits.value.length) % hits.value.length; + activeIndex.value = activeIndex.value < 0 + ? (step === 1 ? 0 : optionCount.value - 1) + : (activeIndex.value + step + optionCount.value) % optionCount.value; }; const choose = (hit: DutyHit) => { @@ -204,6 +220,11 @@ const choose = (hit: DutyHit) => { }; const chooseActive = () => { + if (activeIndex.value === hits.value.length && collection.hasMoreResults.value) { + loadMore(); + return; + } + const hit = hits.value[activeIndex.value]; if (hit) { @@ -211,5 +232,11 @@ const chooseActive = () => { } }; +const loadMore = () => { + if (!isLoadingMore.value) { + collection.loadMore(); + } +}; + const clear = () => emit('update:modelValue', null); diff --git a/resources/js/Features/Admin/Occupancy/__tests__/AssignDutyUserSheet.component.test.ts b/resources/js/Features/Admin/Occupancy/__tests__/AssignDutyUserSheet.component.test.ts index 917a07381..50d9bc6d1 100644 --- a/resources/js/Features/Admin/Occupancy/__tests__/AssignDutyUserSheet.component.test.ts +++ b/resources/js/Features/Admin/Occupancy/__tests__/AssignDutyUserSheet.component.test.ts @@ -234,6 +234,20 @@ describe('AssignDutyUserSheet.vue', () => { describe('extras', () => { const programmes = [{ id: 1, name: 'Ekonomika', tenant_id: 5 }]; + it('shows the original duty title option outside extras without opening them', () => { + const wrapper = mountSheet({ + dutiable: { id: 'd-1', duty_id: 'duty-1', start_date: '2026-01-01', use_original_duty_name: true, duty }, + duty: null, + }); + const checkbox = wrapper.find('#use_original_duty_name'); + const extras = wrapper.find('[data-testid="assign-extras"]'); + + expect(checkbox.exists()).toBe(true); + expect(checkbox.attributes('aria-checked')).toBe('true'); + expect(extras.find('#use_original_duty_name').exists()).toBe(false); + expect(extras.attributes('open')).toBeUndefined(); + }); + it('keeps the extra settings folded away for a plain term', () => { const wrapper = mountSheet(); diff --git a/resources/js/Features/Admin/Occupancy/__tests__/DutySearchField.component.test.ts b/resources/js/Features/Admin/Occupancy/__tests__/DutySearchField.component.test.ts new file mode 100644 index 000000000..0ad2ab40e --- /dev/null +++ b/resources/js/Features/Admin/Occupancy/__tests__/DutySearchField.component.test.ts @@ -0,0 +1,160 @@ +import { mount } from '@vue/test-utils'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ref } from 'vue'; + +import DutySearchField from '@/Features/Admin/Occupancy/DutySearchField.vue'; +import SearchHitRow from '@/Features/Admin/AdminSearch/Components/SearchHitRow.vue'; +import { documentWithMatch } from '@/Shared/Search/matches'; +import { commonStubs } from '@/tests/stubs'; + +const results = ref([]); +const hasMoreResults = ref(false); +const totalHits = ref(0); +const status = ref('idle'); +const loadMore = vi.fn(); +const search = vi.fn(); + +vi.mock('@/Features/Admin/AdminSearch/Composables/useAdminCollectionSearch', () => ({ + useAdminCollectionSearch: () => ({ + results, + hasMoreResults, + totalHits, + status, + isSearching: ref(false), + search, + loadMore, + adminSearch: { + getCollectionTenantIds: () => [5], + isSuperAdmin: ref(false), + }, + }), +})); + +const document = { + id: 'duty-1', + name_lt: 'Atstovas', + name_en: 'Representative', + institution_name_lt: 'Matematikos fakultetas', + institution_name_en: 'Faculty of Mathematics', + tenant_shortname: 'MIF', + home_tenant_id: 5, + type_ids: [3, 8], + is_active: true, + current_user_names: ['Ona Onaitė'], +}; + +const mountField = () => mount(DutySearchField, { + props: { modelValue: null }, + global: { stubs: { ...commonStubs, EntityTypeMark: true } }, +}); + +describe('DutySearchField.vue', () => { + beforeEach(() => { + results.value = [document]; + hasMoreResults.value = false; + totalHits.value = 1; + status.value = 'idle'; + loadMore.mockClear(); + search.mockClear(); + }); + + it('only offers more results when another page exists and shows the counts', async () => { + const wrapper = mountField(); + await wrapper.get('input').setValue('at'); + + expect(wrapper.findAll('[role="option"]')).toHaveLength(1); + expect(wrapper.text()).not.toContain('Rodyti daugiau'); + + hasMoreResults.value = true; + totalHits.value = 9; + await wrapper.vm.$nextTick(); + + expect(wrapper.findAll('[role="option"]')).toHaveLength(2); + expect(wrapper.findAll('[role="option"]').at(-1)!.text()).toContain('Rodyti daugiau'); + }); + + it('loads more on click and blocks another request while loading', async () => { + hasMoreResults.value = true; + const wrapper = mountField(); + await wrapper.get('input').setValue('at'); + const more = wrapper.findAll('[role="option"]').at(-1)!; + + await more.trigger('click'); + expect(loadMore).toHaveBeenCalledTimes(1); + + status.value = 'loading-more'; + await wrapper.vm.$nextTick(); + expect(more.attributes('aria-disabled')).toBe('true'); + expect(more.find('.animate-spin').exists()).toBe(true); + await more.trigger('click'); + expect(loadMore).toHaveBeenCalledTimes(1); + }); + + it('emits the duty payload when a result is chosen', async () => { + const wrapper = mountField(); + await wrapper.get('input').setValue('at'); + await wrapper.get('[role="option"]').trigger('click'); + + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([{ + id: 'duty-1', + name: 'Atstovas', + institutionName: 'Matematikos fakultetas', + tenantShortname: 'MIF', + homeTenantId: 5, + typeIds: [3, 8], + isActive: true, + }]); + }); + + it('shows current holders and forwards raw highlights to the shared row without a chevron', async () => { + results.value = [documentWithMatch({ document, highlights: [ + { field: 'name_lt', snippet: '⟦Atstovas⟧' }, + { field: 'institution_name_lt', snippet: '⟦Matematikos⟧ fakultetas' }, + ] })]; + const wrapper = mountField(); + await wrapper.get('input').setValue('atstovas matematikos'); + + const row = wrapper.getComponent(SearchHitRow); + expect(row.props('hit').raw).toEqual(results.value[0]); + expect(row.props('trailing')).toBe('none'); + expect(row.text()).toContain('Ona Onaitė'); + expect(row.find('[data-field="institution_name_lt"] mark').text()).toBe('Matematikos'); + expect(row.findAll('mark').some(mark => mark.text().includes('Atstov'))).toBe(true); + }); + + it('reaches show more with the arrows and loads it with Enter', async () => { + hasMoreResults.value = true; + const wrapper = mountField(); + const input = wrapper.get('input'); + await input.setValue('at'); + await input.trigger('keydown', { key: 'ArrowUp' }); + + const more = wrapper.findAll('[role="option"]').at(-1)!; + expect(input.attributes('aria-activedescendant')).toBe(more.attributes('id')); + await input.trigger('keydown', { key: 'Enter' }); + expect(loadMore).toHaveBeenCalledTimes(1); + expect(wrapper.emitted('update:modelValue')).toBeUndefined(); + + await input.trigger('keydown', { key: 'ArrowUp' }); + expect(wrapper.findAll('[role="option"]')[0].attributes('aria-selected')).toBe('true'); + await input.trigger('keydown', { key: 'ArrowDown' }); + expect(more.attributes('aria-selected')).toBe('true'); + }); + + it('keeps the active duty when another page is appended', async () => { + hasMoreResults.value = true; + const wrapper = mountField(); + const input = wrapper.get('input'); + await input.setValue('at'); + await input.trigger('keydown', { key: 'ArrowDown' }); + const activeId = input.attributes('aria-activedescendant'); + + results.value = [...results.value, { ...document, id: 'duty-2' }]; + await wrapper.vm.$nextTick(); + + expect(input.attributes('aria-activedescendant')).toBe(activeId); + expect(wrapper.findAll('[role="option"]')[0].attributes('aria-selected')).toBe('true'); + await input.trigger('keydown', { key: 'Enter' }); + expect(wrapper.emitted('update:modelValue')?.[0]?.[0]).toMatchObject({ id: 'duty-1' }); + }); +}); diff --git a/tests/Feature/Search/SearchExperienceTest.php b/tests/Feature/Search/SearchExperienceTest.php index 3fb491606..43919f58f 100644 --- a/tests/Feature/Search/SearchExperienceTest.php +++ b/tests/Feature/Search/SearchExperienceTest.php @@ -128,6 +128,22 @@ expect(SearchText::forModel($user))->toBe(['search_text_lt' => '', 'search_text_en' => '']); }); +test('duty search text combines duty and institution names with the tenant shortname in each locale', function (): void { + $tenant = Tenant::query()->where('alias', 'mif')->firstOrFail(); + $institution = Institution::factory()->for($tenant)->create([ + 'name' => ['lt' => 'Matematikos fakultetas', 'en' => 'Faculty of Mathematics'], + ]); + $duty = Duty::factory()->for($institution)->create([ + 'name' => ['lt' => 'Atstovas', 'en' => 'Representative'], + 'description' => ['lt' => '', 'en' => ''], + ]); + + $text = SearchText::forModel($duty); + + expect($text['search_text_lt'])->toContain('Atstovas', 'Matematikos fakultetas', $tenant->shortname) + ->and($text['search_text_en'])->toContain('Representative', 'Faculty of Mathematics', $tenant->shortname); +}); + test('committed block edits and removals update admin and public bodies', function (string $model, string $mirror): void { usesTypesense(); config(['scout.queue' => false]); From a4ca2d2daf6221f5b9da824fd8b9921658c29aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Justinas=20Kavoli=C5=ABnas?= Date: Fri, 6 Nov 2026 20:56:59 +0200 Subject: [PATCH 2/9] Search pickers: one shared result row, a few contextual shells - InlineSearchField: the inline combobox shell (keyboard nav, show more, selected card) rendering SearchHitRow; DutySearchField and MemberSearchField are now thin wrappers with unchanged APIs. - UserSearchField: people from api.v1.admin.users.search (with create-new), mapped via normalizeUserApiHit; ProblemForm's hand-rolled reka combobox for the responsible person now uses it. - useSearchHitMatches + SearchHitCardText: ActionWindow institution search cards show highlighted matches and status like SearchHitRow. - InvolvedUsersPicker shows email under the name when the payload has it (the support-request payloads don't include email today). - pickers.md rule: don't hand-roll search result rows. Changelog and guides. Written by Codex. Claude review: restored constraint comments Codex trimmed in InstitutionSearchScreen, braces on one-line ifs, EN changelog wording. Co-Authored-By: Claude Opus 5.5 --- .ai/rules/pickers.md | 6 + docs/changelog/v3.md | 1 + docs/en/changelog/v3.md | 1 + docs/organizacija/pareigybes.md | 3 +- docs/pagrindai/platforma.md | 4 + docs/visak/problemos.md | 6 +- .../screens/InstitutionSearchScreen.vue | 14 +- .../js/Components/AdminForms/ProblemForm.vue | 123 +------- .../__tests__/ProblemForm.component.test.ts | 17 +- .../SupportRequests/InvolvedUsersPicker.vue | 5 +- .../Components/SearchHitCardText.vue | 28 ++ .../AdminSearch/Components/SearchHitRow.vue | 21 +- .../Components/Select/InlineSearchField.vue | 166 ++++++++++ .../Components/Select/UserSearchField.vue | 201 ++++++++++++ .../AdminSearch/Components/Select/index.ts | 2 + .../Composables/useSearchHitMatches.ts | 21 ++ .../AdminSearch/Utils/searchHitMappers.ts | 25 ++ .../Admin/Occupancy/DutySearchField.vue | 184 ++--------- .../Admin/Occupancy/MemberSearchField.vue | 297 +----------------- 19 files changed, 542 insertions(+), 583 deletions(-) create mode 100644 resources/js/Features/Admin/AdminSearch/Components/SearchHitCardText.vue create mode 100644 resources/js/Features/Admin/AdminSearch/Components/Select/InlineSearchField.vue create mode 100644 resources/js/Features/Admin/AdminSearch/Components/Select/UserSearchField.vue create mode 100644 resources/js/Features/Admin/AdminSearch/Composables/useSearchHitMatches.ts diff --git a/.ai/rules/pickers.md b/.ai/rules/pickers.md index 436707e2f..531aa44e7 100644 --- a/.ai/rules/pickers.md +++ b/.ai/rules/pickers.md @@ -42,3 +42,9 @@ Extends the picker rules above. - Several of many → `MultiSelect` chips; tags → `TagMultiSelect`. - One picker per data kind, so never a second date picker. The trigger shows readable text plus context, never an id. Optional fields get "Išvalyti". Lithuanian locale: Monday first, 24h. - No dialog inside a dialog: on mobile, a picker opened from a sheet replaces the sheet's content, with a back action. + +## One search-hit presentation, contextual shells +Search hits render `SearchHitRow` from a `NormalizedSearchHit`; do not hand-roll result rows. +Use `SearchResultList` for dialogs and `CommandItem` for the palette. +Use `InlineSearchField` for inline pickers in sheets/forms, with `UserSearchField` for people from the users API. +Keep ActionWindow cards contextual, sharing matches through `useSearchHitMatches`. diff --git a/docs/changelog/v3.md b/docs/changelog/v3.md index 9137b261e..b0806416f 100644 --- a/docs/changelog/v3.md +++ b/docs/changelog/v3.md @@ -34,6 +34,7 @@ Datos platformoje ir vusa.lt rodomos vienodai ir visada Vilniaus laiku. Gide: [D ### Kiti pakeitimai ir pataisymai +- ✨ **Vienodi paieškos rezultatai** – problemos atsakingo asmens ir nario pasirinkimas bei institucijų paieška veiksmų lange atrodo kaip kitur platformoje; sutapę žodžiai institucijų paieškoje paryškinami. Gide: [Įrašų pasirinkimas](/pagrindai/platforma#pasirinkti-susijusi-irasa) - 🔧 **Pareigybės paieška** – randa pareigybes pagal kartu įrašytus pareigybės ir institucijos ar padalinio žodžius, pvz., „atstovas matematikos“ arba „pirmininkas MIF“. Gide: [Pareigybės pasirinkimas](/organizacija/pareigybes#priskirti-nari) - ✨ **„Rodyti daugiau“** – pareigybės pasirinkimo sąrašą galima papildyti tame pačiame lange. Sutapę žodžiai paryškinami, rodomi dabartiniai pareigas einantys nariai. Gide: [Pareigybės pasirinkimas](/organizacija/pareigybes#priskirti-nari) - ✨ **Originalus pareigybės pavadinimas** – parinktis „Viešame puslapyje naudoti originalų pareigybės pavadinimą“ matoma iškart po pareigybe, neatveriant „Papildomi nustatymai“. Gide: [Nario priskyrimas](/organizacija/pareigybes#priskirti-nari) diff --git a/docs/en/changelog/v3.md b/docs/en/changelog/v3.md index e589e4b4e..dcdb392fa 100644 --- a/docs/en/changelog/v3.md +++ b/docs/en/changelog/v3.md @@ -34,6 +34,7 @@ Dates on the platform and vusa.lt now read the same everywhere and always in Vil ### Other changes and fixes +- ✨ **Consistent search results** – the problem responsible person and member pickers and institution search in the action window now look like search elsewhere; matching words in institution search are highlighted. Guide: [Choosing records](/pagrindai/platforma#pasirinkti-susijusi-irasa) - 🔧 **Duty search** – finds duties using duty and institution or unit words together, such as “atstovas matematikos” or “pirmininkas MIF”. Guide: [Choosing a duty](/organizacija/pareigybes#priskirti-nari) - ✨ **“Show more”** – adds more matches to the duty picker in the same window. Matching words are highlighted and current duty holders are shown. Guide: [Choosing a duty](/organizacija/pareigybes#priskirti-nari) - ✨ **Original duty title** – the option to use the original duty title on the public website is visible directly below the duty, without opening “Additional settings”. Guide: [Assigning a member](/organizacija/pareigybes#priskirti-nari) diff --git a/docs/organizacija/pareigybes.md b/docs/organizacija/pareigybes.md index e2423390d..eaed859ba 100644 --- a/docs/organizacija/pareigybes.md +++ b/docs/organizacija/pareigybes.md @@ -390,7 +390,8 @@ parodo, kiek jau rodoma ir kiek iš viso rasta; ją pasirinkus sąrašas papildo Paieška randa pareigybes pagal kartu įrašytus pareigybės ir institucijos ar padalinio žodžius, o daugiau rezultatų galima parodyti tame pačiame sąraše. Originalaus pavadinimo parinktis matoma -iškart po pareigybe. +iškart po pareigybe. Nario paieškos rezultatuose, kaip ir kitose pasirinkimo vietose, rodomas vardas, +el. paštas, kai jis prieinamas, ir padaliniai. diff --git a/docs/pagrindai/platforma.md b/docs/pagrindai/platforma.md index 9bf31d747..8eb52f49f 100644 --- a/docs/pagrindai/platforma.md +++ b/docs/pagrindai/platforma.md @@ -150,6 +150,10 @@ Peržiūroje klavišais **↑** ir **↓** pereinama prie kito įrašo, o **Ente ### Pasirinkti susijusį įrašą +Nario ir problemos atsakingo asmens paieškos rezultatai pateikiami tokiomis pačiomis eilutėmis +kaip bendra paieška. Institucijų paieška veiksmų lange išlaiko pasirinkimo korteles, o sutapę +žodžiai pavadinime ar aprašyme paryškinami. + Kai forma prašo susieti kitą įrašą, pasirinkimo lange galima ieškoti, filtruoti ir peržiūrėti radinį prieš jį pridedant. Pažymėti įrašai patvirtinami lango apačioje; jei laukas neprivalomas, pasirinkimą galima išvalyti. Telefone peržiūrėjus radinį mygtuku „Atgal į sąrašą“ grįžtama prie rezultatų. Nepasiekiamą išteklių galima peržiūrėti, bet negalima pasirinkti. Kuriant narį ar pareigybę, perspėjimas apie galimą dublikatą parodo sutapimus ir galimus veiksmus. Tai patarimas: sutampantys vardai savaime neuždraudžia išsaugoti įrašo. Atverti ar sujungti kitą įrašą siūloma tik tada, kai turi teisę jį tvarkyti. diff --git a/docs/visak/problemos.md b/docs/visak/problemos.md index 55de5e962..152163320 100644 --- a/docs/visak/problemos.md +++ b/docs/visak/problemos.md @@ -1,7 +1,7 @@ --- doc_status: reviewed title: Problemos -last_reviewed: 2026-11-01 +last_reviewed: 2026-11-06 tests: - tests/Feature/Admin/Problems/ProblemControllerTest.php - tests/Feature/Admin/Problems/AgendaItemProblemControllerTest.php @@ -110,6 +110,10 @@ jos problemos. (ieškoma pagal vardą, bent 2 simboliai). Sąrašo greitasis filtras **Man priskirtos** rodo problemas, kurių atsakingas asmuo esi tu. +Rezultatai rodomi po paieškos lauku: prie vardo pateikiamas el. paštas, kai jis prieinamas, +ir padaliniai. Rodyklėmis **↑ / ↓** galima pažymėti narį, o **Enter** jį pasirinkti. +Pasirinktas narys rodomas atskirai; mygtuku **Pakeisti narį** pasirinkimas išvalomas. + ### Kas mato problemas Problemas mato **kiekvienas narys, net be rolės**: visų padalinių problemos yra bendra žinių bazė. diff --git a/resources/js/Components/ActionWindow/screens/InstitutionSearchScreen.vue b/resources/js/Components/ActionWindow/screens/InstitutionSearchScreen.vue index 24aec1345..6682908f7 100644 --- a/resources/js/Components/ActionWindow/screens/InstitutionSearchScreen.vue +++ b/resources/js/Components/ActionWindow/screens/InstitutionSearchScreen.vue @@ -35,8 +35,11 @@ :icon="Landmark" @click="pick(hit)" > + @@ -63,7 +66,7 @@ import ActionChoiceButton from '../ActionChoiceButton.vue'; import ActionChoiceList from '../ActionChoiceList.vue'; import ActionWindowScreen from '../ActionWindowScreen.vue'; -import InactiveInstitutionBadge from '@/Components/Institutions/InactiveInstitutionBadge.vue'; +import SearchHitCardText from '@/Features/Admin/AdminSearch/Components/SearchHitCardText.vue'; import { useActionWindow } from '@/Composables/useActionWindow'; import { useActionWindowData } from '@/Composables/useActionWindowData'; import { useAdminCollectionSearch } from '@/Features/Admin/AdminSearch/Composables/useAdminCollectionSearch'; @@ -78,9 +81,8 @@ const { institutionSearch } = useActionWindowData(); /** * The tenants the caller may file a meeting under. All-scope callers send no tenant filter — - * the scoped Typesense key still bounds what they may read, and StoreMeetingRequest is what - * actually decides. From a calendar event, only VU SA's own bodies (mirrors - * InstitutionScopeResolver::forInstitution()). + * the scoped Typesense key still bounds what they may read, and StoreMeetingRequest decides. + * From a calendar event, only VU SA's own bodies (mirrors InstitutionScopeResolver::forInstitution()). */ const buildBaseFilterBy = (): string | undefined => { const { tenant_ids, internal_type_ids, external_type_ids } = institutionSearch.value; @@ -103,8 +105,6 @@ const buildBaseFilterBy = (): string | undefined => { const baseFilterBy = buildBaseFilterBy(); -// A plain list, not the admin search page's split view: this window is one question -// per screen, and facets/detail panes have no room in it. const controller = useAdminCollectionSearch({ collection: 'institutions', syncToUrl: false, diff --git a/resources/js/Components/AdminForms/ProblemForm.vue b/resources/js/Components/AdminForms/ProblemForm.vue index 82db51121..b53332f53 100644 --- a/resources/js/Components/AdminForms/ProblemForm.vue +++ b/resources/js/Components/AdminForms/ProblemForm.vue @@ -107,54 +107,14 @@ :label="capitalize($tChoice('entities.problem.responsible_user', 1))" :error="form.errors.responsible_user_id" > - - - - - - - - -
    - {{ $t('Įvesk bent 2 simbolius') }} -
    -
    - {{ $t('Ieškoma...') }} -
    - -
    -
    -
    + />
    @@ -259,15 +219,8 @@ diff --git a/resources/js/Features/Admin/AdminSearch/Components/SearchHitRow.vue b/resources/js/Features/Admin/AdminSearch/Components/SearchHitRow.vue index 03b7be8e3..ecb855e3a 100644 --- a/resources/js/Features/Admin/AdminSearch/Components/SearchHitRow.vue +++ b/resources/js/Features/Admin/AdminSearch/Components/SearchHitRow.vue @@ -11,7 +11,6 @@
    -
    @@ -26,7 +25,6 @@ {{ $t('Neseniai') }} - - +
    -
    - + + +
    + + + + +
    + + +
    + + + +
    + + +
      +
    • + +
    • +
    • +
    • +
    • + {{ searching ? $t('Ieškoma…') : emptyMessage }} +
    • + +
    +
    +

    {{ error }}

    +
  • + + + diff --git a/resources/js/Features/Admin/AdminSearch/Components/Select/UserSearchField.vue b/resources/js/Features/Admin/AdminSearch/Components/Select/UserSearchField.vue new file mode 100644 index 000000000..4a49a60c7 --- /dev/null +++ b/resources/js/Features/Admin/AdminSearch/Components/Select/UserSearchField.vue @@ -0,0 +1,201 @@ + + + diff --git a/resources/js/Features/Admin/AdminSearch/Components/Select/index.ts b/resources/js/Features/Admin/AdminSearch/Components/Select/index.ts index 888ba3d79..7cd061ecf 100644 --- a/resources/js/Features/Admin/AdminSearch/Components/Select/index.ts +++ b/resources/js/Features/Admin/AdminSearch/Components/Select/index.ts @@ -2,3 +2,5 @@ export { default as CollectionSelectDialog } from './CollectionSelectDialog.vue' export { default as MultiCollectionSelectDialog } from './MultiCollectionSelectDialog.vue'; export { default as InlineCollectionSelect } from './InlineCollectionSelect.vue'; export { default as ResourceSelectDialog } from './ResourceSelectDialog.vue'; +export { default as InlineSearchField } from './InlineSearchField.vue'; +export { default as UserSearchField } from './UserSearchField.vue'; diff --git a/resources/js/Features/Admin/AdminSearch/Composables/useSearchHitMatches.ts b/resources/js/Features/Admin/AdminSearch/Composables/useSearchHitMatches.ts new file mode 100644 index 000000000..cdfee64c0 --- /dev/null +++ b/resources/js/Features/Admin/AdminSearch/Composables/useSearchHitMatches.ts @@ -0,0 +1,21 @@ +import { computed, toValue, type MaybeRefOrGetter } from 'vue'; + +import type { NormalizedSearchHit } from '../Utils/searchHitMappers'; + +import { highlightText, matchTitle, type SearchMatchDocument } from '@/Shared/Search/matches'; + +export function useSearchHitMatches(hit: MaybeRefOrGetter) { + const matches = computed(() => toValue(hit).raw as SearchMatchDocument); + const titleMatch = computed(() => matchTitle(toValue(hit).title, matches.value._searchTitleMatches)); + const fieldMatches = computed(() => Object.values(matches.value._searchFieldMatches ?? {})); + const highlightIn = (text?: string) => text ? fieldMatches.value.map(match => highlightText(text, match)).find(Boolean) : undefined; + const metaMatch = computed(() => highlightIn(toValue(hit).meta)); + const contextMatch = computed(() => { + const inSubtitle = highlightIn(toValue(hit).subtitle); + if (inSubtitle || titleMatch.value || metaMatch.value) return inSubtitle; + // A hidden-field match explains why the result is here when no visible text matched. + return matches.value._searchMatch ?? fieldMatches.value[0]; + }); + + return { matches, titleMatch, metaMatch, contextMatch }; +} diff --git a/resources/js/Features/Admin/AdminSearch/Utils/searchHitMappers.ts b/resources/js/Features/Admin/AdminSearch/Utils/searchHitMappers.ts index 0eda75e6d..37f715d37 100644 --- a/resources/js/Features/Admin/AdminSearch/Utils/searchHitMappers.ts +++ b/resources/js/Features/Admin/AdminSearch/Utils/searchHitMappers.ts @@ -132,6 +132,31 @@ export interface NormalizedSearchHit { raw: unknown; } +export function normalizeUserApiHit(user: { + id: string; + name: string; + email?: string; + profile_photo_path?: string | null; + tenants?: string[]; + is_current?: boolean; + last_term_year?: number | null; +}, { taken = false }: { taken?: boolean } = {}): NormalizedSearchHit { + const tenants = user.tenants?.join(', ') || undefined; + return { + id: `users-${user.id}`, + recordId: String(user.id), + collection: 'users', + icon: UserIcon, + title: user.name, + imageUrl: user.profile_photo_path || undefined, + subtitle: taken ? $t('Jau eina šias pareigas') : user.email, + meta: user.is_current === false && user.last_term_year ? `${$t('Buvęs narys')} · ${user.last_term_year}` : undefined, + badge: tenants, + contextBadge: tenants, + raw: user, + }; +} + /** Per-collection display metadata (icon + label + page tab). */ export const COLLECTION_META: Record -
    - - -
    -
    - -
    -

    - -

    -

    - {{ modelValue.institutionName }} -

    -
    -
    - -
    - - - -

    - {{ error }} -

    -
    + diff --git a/resources/js/Features/Admin/Occupancy/MemberSearchField.vue b/resources/js/Features/Admin/Occupancy/MemberSearchField.vue index 731423134..805b17c04 100644 --- a/resources/js/Features/Admin/Occupancy/MemberSearchField.vue +++ b/resources/js/Features/Admin/Occupancy/MemberSearchField.vue @@ -1,297 +1,30 @@ From fc80bdbd3d792c80132afd83e956ddf4c09f1bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Justinas=20Kavoli=C5=ABnas?= Date: Fri, 6 Nov 2026 21:10:26 +0200 Subject: [PATCH 3/9] Lighter search surfaces and one selection mark size - AssignDutyUserSheet: "use original duty title" sits below the dates. - SearchSplitView panes are bg-card, so picker results no longer sit on the grey paper inside a white dialog; ActionChoiceButton is bg-card in light mode. - SearchResultListItem: one 18px square mark for both modes (check for many, filled square for one) inside a 32/44px target, with checkbox/radio roles. Co-Authored-By: Claude Opus 5.5 --- docs/changelog/v3.md | 4 +- docs/en/changelog/v3.md | 4 +- docs/organizacija/pareigybes.md | 12 ++--- .../ActionWindow/ActionChoiceButton.vue | 2 +- .../Components/SearchResultListItem.vue | 46 ++++++++++--------- .../Components/SearchSplitView.vue | 4 +- .../SearchResultListItem.component.test.ts | 10 ++-- .../Admin/Occupancy/AssignDutyUserSheet.vue | 34 +++++++------- 8 files changed, 61 insertions(+), 55 deletions(-) diff --git a/docs/changelog/v3.md b/docs/changelog/v3.md index b0806416f..e662c6e68 100644 --- a/docs/changelog/v3.md +++ b/docs/changelog/v3.md @@ -34,10 +34,10 @@ Datos platformoje ir vusa.lt rodomos vienodai ir visada Vilniaus laiku. Gide: [D ### Kiti pakeitimai ir pataisymai -- ✨ **Vienodi paieškos rezultatai** – problemos atsakingo asmens ir nario pasirinkimas bei institucijų paieška veiksmų lange atrodo kaip kitur platformoje; sutapę žodžiai institucijų paieškoje paryškinami. Gide: [Įrašų pasirinkimas](/pagrindai/platforma#pasirinkti-susijusi-irasa) +- ✨ **Vienodi paieškos rezultatai** – problemos atsakingo asmens ir nario pasirinkimas bei institucijų paieška veiksmų lange atrodo kaip kitur platformoje; sutapę žodžiai institucijų paieškoje paryškinami. Šviesiame režime rezultatai ir veiksmų lango pasirinkimai rodomi šviesiame fone, o pažymėjimo langeliai visur vienodo dydžio. Gide: [Įrašų pasirinkimas](/pagrindai/platforma#pasirinkti-susijusi-irasa) - 🔧 **Pareigybės paieška** – randa pareigybes pagal kartu įrašytus pareigybės ir institucijos ar padalinio žodžius, pvz., „atstovas matematikos“ arba „pirmininkas MIF“. Gide: [Pareigybės pasirinkimas](/organizacija/pareigybes#priskirti-nari) - ✨ **„Rodyti daugiau“** – pareigybės pasirinkimo sąrašą galima papildyti tame pačiame lange. Sutapę žodžiai paryškinami, rodomi dabartiniai pareigas einantys nariai. Gide: [Pareigybės pasirinkimas](/organizacija/pareigybes#priskirti-nari) -- ✨ **Originalus pareigybės pavadinimas** – parinktis „Viešame puslapyje naudoti originalų pareigybės pavadinimą“ matoma iškart po pareigybe, neatveriant „Papildomi nustatymai“. Gide: [Nario priskyrimas](/organizacija/pareigybes#priskirti-nari) +- ✨ **Originalus pareigybės pavadinimas** – parinktis „Viešame puslapyje naudoti originalų pareigybės pavadinimą“ matoma iškart po pareigų datomis, neatveriant „Papildomi nustatymai“. Gide: [Nario priskyrimas](/organizacija/pareigybes#priskirti-nari) - 🔧 **Registracijų eksportas** – narių ir studentų atstovų registracijų eksporte pateikiamos tik tos registracijos, kurias naudotojas gali matyti formos puslapyje. Gide: [Atsakymų eksportavimas](/organizacija/formos-ir-registracijos#eksportas) - 🔧 **Dokumentų tikrinimas** – sėkmingai tikrinami dokumentai ir toliau automatiškai atnaujinami, o pakartotinai nepavykę patikrinimai neužima kitų dokumentų tikrinimo vietų. Gide: [Dokumentai](/visak/dokumentai#pranesimai) - 🔧 **Paieška** – jei per trumpą laiką ieškota per daug kartų, paieška vėl veikia po kelių sekundžių, nebereikia perkrauti puslapio. Sąrašai atveriami su nuorodoje pasirinkta paieška ir filtrais nuo pirmojo rezultatų įkėlimo. Paieškos lange veiksmas „Paklausti, ar vyko posėdžiai“ atveria užklausos kūrimą, o ne pradinį veiksmų langą. Gide: [Rasti skiltį ar įrašą](/pagrindai/platforma#bendra-paieska) diff --git a/docs/en/changelog/v3.md b/docs/en/changelog/v3.md index dcdb392fa..c99e8e75e 100644 --- a/docs/en/changelog/v3.md +++ b/docs/en/changelog/v3.md @@ -34,10 +34,10 @@ Dates on the platform and vusa.lt now read the same everywhere and always in Vil ### Other changes and fixes -- ✨ **Consistent search results** – the problem responsible person and member pickers and institution search in the action window now look like search elsewhere; matching words in institution search are highlighted. Guide: [Choosing records](/pagrindai/platforma#pasirinkti-susijusi-irasa) +- ✨ **Consistent search results** – the problem responsible person and member pickers and institution search in the action window now look like search elsewhere; matching words in institution search are highlighted. In light mode, results and action window choices sit on a lighter background, and selection boxes are the same size everywhere. Guide: [Choosing records](/pagrindai/platforma#pasirinkti-susijusi-irasa) - 🔧 **Duty search** – finds duties using duty and institution or unit words together, such as “atstovas matematikos” or “pirmininkas MIF”. Guide: [Choosing a duty](/organizacija/pareigybes#priskirti-nari) - ✨ **“Show more”** – adds more matches to the duty picker in the same window. Matching words are highlighted and current duty holders are shown. Guide: [Choosing a duty](/organizacija/pareigybes#priskirti-nari) -- ✨ **Original duty title** – the option to use the original duty title on the public website is visible directly below the duty, without opening “Additional settings”. Guide: [Assigning a member](/organizacija/pareigybes#priskirti-nari) +- ✨ **Original duty title** – the option to use the original duty title on the public website is visible right below the dates, without opening “Additional settings”. Guide: [Assigning a member](/organizacija/pareigybes#priskirti-nari) - 🔧 **Registration exports** – member and student representative registration exports now include only the registrations the user can see on the form page. Guide: [Exporting responses](/organizacija/formos-ir-registracijos#eksportas) - 🔧 **Document checks** – documents with successful checks continue to refresh automatically, and repeatedly failed checks no longer take up other documents’ refresh slots. Guide: [Documents](/visak/dokumentai#pranesimai) - 🔧 **Search** – after too many searches in a short time, search works again within a few seconds instead of needing a page reload. Lists apply the search and filters from the link on their first load. In the search window, the „Ask whether meetings happened“ action opens the request itself instead of the starting action window. Guide: [Finding a section or record](/pagrindai/platforma#bendra-paieska) diff --git a/docs/organizacija/pareigybes.md b/docs/organizacija/pareigybes.md index eaed859ba..9268a460c 100644 --- a/docs/organizacija/pareigybes.md +++ b/docs/organizacija/pareigybes.md @@ -366,16 +366,16 @@ Užbaigimo patvirtinimas aiškiai paaiškina, kad šiandien pareigos dar galioja ### Nario priskyrimas ir šalinimas {#priskirti-nari} Paspaudus **Priskirti** pareigybės puslapyje: -1. Po pareigybės pavadinimu galima pažymėti **Viešame puslapyje naudoti originalų pareigybės pavadinimą**. - Ši parinktis matoma iškart, neatveriant papildomų nustatymų. -2. Pasirenkamas narys. Paieškoje pirmiausia rodomi dabartiniai nariai, o buvę pažymimi „Buvęs narys · metai“. +1. Pasirenkamas narys. Paieškoje pirmiausia rodomi dabartiniai nariai, o buvę pažymimi „Buvęs narys · metai“. Jei žmogaus sistemoje dar nėra, jį galima sukurti čia pat – užtenka vardo ir pavardės, o el. paštas nebūtinas (aktualu buvusiems nariams; be el. pašto jie tiesiog negauna laiškų). Sukurti naują narį gali tie, kurie turi teisę kurti narius. -3. Pasirenkama **kadencija**. Pagal nutylėjimą – dabartinė: pradžia šiandien, pabaiga – kadencijos +2. Pasirenkama **kadencija**. Pagal nutylėjimą – dabartinė: pradžia šiandien, pabaiga – kadencijos pabaiga. Pasirinkus ankstesnę kadenciją, datos užpildomos visam jos laikotarpiui. Kadencijos imamos iš institucijos [kadencijų](/sistema/nustatymai#kadencijos), o jei institucija savų neturi – iš bendrųjų. -4. Jei reikia, datos pataisomos ranka – tada pasirinkimas tampa „Kitos datos“. +3. Jei reikia, datos pataisomos ranka – tada pasirinkimas tampa „Kitos datos“. +4. Po datomis galima pažymėti **Viešame puslapyje naudoti originalų pareigybės pavadinimą** – + parinktis matoma iškart, neatveriant papildomų nustatymų. 5. **Papildomi nustatymai** (suskleisti): papildomas el. paštas, nuotrauka ir **Papildomas aprašymas**, rodomas viešame kontaktų sąraše vietoj pareigybės aprašymo. Kuratorių pareigybėms čia rodoma ir studijų programa bei jos pastaba. @@ -390,7 +390,7 @@ parodo, kiek jau rodoma ir kiek iš viso rasta; ją pasirinkus sąrašas papildo Paieška randa pareigybes pagal kartu įrašytus pareigybės ir institucijos ar padalinio žodžius, o daugiau rezultatų galima parodyti tame pačiame sąraše. Originalaus pavadinimo parinktis matoma -iškart po pareigybe. Nario paieškos rezultatuose, kaip ir kitose pasirinkimo vietose, rodomas vardas, +iškart po pareigų datomis. Nario paieškos rezultatuose, kaip ir kitose pasirinkimo vietose, rodomas vardas, el. paštas, kai jis prieinamas, ir padaliniai. diff --git a/resources/js/Components/ActionWindow/ActionChoiceButton.vue b/resources/js/Components/ActionWindow/ActionChoiceButton.vue index 963662911..22df93832 100644 --- a/resources/js/Components/ActionWindow/ActionChoiceButton.vue +++ b/resources/js/Components/ActionWindow/ActionChoiceButton.vue @@ -6,7 +6,7 @@ :aria-pressed="selected ? 'true' : undefined" :class="cn( 'group relative flex w-full items-center justify-between gap-3.5 border p-3.5 sm:p-4 text-left min-h-14 transition-all duration-150', - 'border-border/50 bg-secondary/35 hover:border-brand/60 hover:bg-secondary/60 dark:bg-secondary/20 dark:hover:bg-secondary/40', + 'border-border bg-card hover:border-brand/60 hover:bg-secondary/40 dark:border-border/50 dark:bg-secondary/20 dark:hover:bg-secondary/40', 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring', 'disabled:pointer-events-none disabled:opacity-50', selected && 'border-brand bg-brand/10 dark:bg-brand/20 hover:bg-brand/15 dark:hover:bg-brand/25', diff --git a/resources/js/Features/Admin/AdminSearch/Components/SearchResultListItem.vue b/resources/js/Features/Admin/AdminSearch/Components/SearchResultListItem.vue index e9e03cded..6a34cd655 100644 --- a/resources/js/Features/Admin/AdminSearch/Components/SearchResultListItem.vue +++ b/resources/js/Features/Admin/AdminSearch/Components/SearchResultListItem.vue @@ -15,26 +15,28 @@ @keydown.enter.prevent="!disabled && $emit('toggle')" @keydown.space.prevent="!disabled && $emit('toggle')" > -
    - - -
    + + + + @@ -53,18 +55,18 @@ From ed2d3900930f76956cc865cb1eb7af4c236e710f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Justinas=20Kavoli=C5=ABnas?= Date: Fri, 6 Nov 2026 21:21:29 +0200 Subject: [PATCH 5/9] ActionWindow: chevrons for steps, check boxes only for in-place choices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vue casts an omitted boolean prop to false, so ActionChoiceButton's `selected !== undefined` was always true: every card, even on "Ką norėtum padaryti?", drew an empty check box instead of a chevron. `showChevron` is replaced by `indicator: 'chevron' | 'check'`. Steps that move on keep the chevron and mark the earlier answer with a brand outline and aria-current; the activity-request institution/people toggles and the agenda item type dialog use check boxes with aria-pressed. Co-Authored-By: Claude Opus 5.5 --- .../ActionWindow/ActionChoiceButton.vue | 17 ++++++------ .../ActionWindow/ActionWindow.stories.ts | 6 ++--- .../ActionChoiceButton.component.test.ts | 26 ++++++++++++++++--- .../CheckInUntilScreen.component.test.ts | 4 +-- .../MeetingWhenScreen.component.test.ts | 4 +-- .../screens/ActivityRequestCampaignScreen.vue | 4 +-- .../ActivityRequestInstitutionsScreen.vue | 1 + .../screens/ActivityRequestPeopleScreen.vue | 1 + .../screens/CheckInUntilScreen.vue | 3 +-- .../screens/InstitutionPickerScreen.vue | 2 +- .../screens/InstitutionSearchScreen.vue | 2 +- .../screens/MeetingTimeScreen.vue | 1 - .../screens/MeetingTypeScreen.vue | 1 - .../screens/MeetingWhenScreen.vue | 3 +-- .../AgendaItems/AgendaItemTypeDialog.vue | 2 +- .../AgendaItemTypeDialog.component.test.ts | 2 +- 16 files changed, 48 insertions(+), 31 deletions(-) diff --git a/resources/js/Components/ActionWindow/ActionChoiceButton.vue b/resources/js/Components/ActionWindow/ActionChoiceButton.vue index f3759574c..dc9123b35 100644 --- a/resources/js/Components/ActionWindow/ActionChoiceButton.vue +++ b/resources/js/Components/ActionWindow/ActionChoiceButton.vue @@ -3,7 +3,8 @@ type="button" data-slot="action-choice-button" :disabled - :aria-pressed="selected ? 'true' : undefined" + :aria-pressed="indicator === 'check' ? String(!!selected) : undefined" + :aria-current="indicator === 'chevron' && selected ? 'true' : undefined" :class="cn( 'group relative flex w-full items-center justify-between gap-3.5 border p-3.5 sm:p-4 text-left min-h-14 transition-all duration-150', 'border-border bg-card hover:border-brand/60 hover:bg-secondary/40 dark:border-border/50 dark:bg-secondary/20 dark:hover:bg-secondary/40', @@ -34,7 +35,7 @@