adde table and editor components

This commit is contained in:
2026-01-15 17:29:12 -05:00
parent 099d48621b
commit d8ad1e3804
13 changed files with 493 additions and 22 deletions

View File

@@ -0,0 +1,279 @@
<script setup lang="ts">
import { BButton, BCol, BContainer, BFormGroup, BFormInput, BFormSelect, BInputGroup, BInputGroupText, BPagination, BRow, BSpinner, BTable, type TableFieldRaw, type TableItem } from 'bootstrap-vue-next'
import type { GlossaryItem } from '@/types/GlossaryItem'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useGlossaryEntryStore } from '@/stores/glossaryEntry'
import { onMounted, ref, type Ref } from 'vue'
// use global scope
const { t } = useI18n({
useScope: 'global',
inheritLocale: true,
})
const router = useRouter()
const glossaryEntryStore = useGlossaryEntryStore()
const isLoading = ref(false)
const filter = ref('')
const glossaryEntries: Ref<TableItem<GlossaryItem>[]> = ref([])
const currentPage = ref(1)
const perPage = ref (5)
const totalRows = ref(glossaryEntries.value.length)
const pageOptions = [
{value: 5, text: '5'},
{value: 10, text: '10'},
{value: 15, text: '15'},
{value: 100, text: '100'},
]
/**
* Saves the passed table item (a GlossaryItem object) to the
* glossaryEntry store and routes the user to the edit page
* @param item GlossaryItem object from the table
* @param index Row index of the GlossaryItem object in the table
*/
const goToEdit = (item: TableItem<GlossaryItem>, index: number) => {
glossaryEntryStore.setGlossaryEntry({
uuid: item.uuid,
title: item.title,
description: item.description,
source: item.source,
})
router.push({ path: '/edit' })
}
/**
* Filters the results displayed in the table only against certain
* columns. This avoid the default filter function, which will
* filter on UUID.
* @param item Item from the table
*/
const filterFuntion = (item: Readonly<GlossaryItem>): boolean => {
if (typeof filter == undefined) return false
if (
filter.value == '' ||
item.title.includes(filter.value) ||
item.description.includes(filter.value) ||
item.source.includes(filter.value)
) {
return true
}
return false
}
const onFiltered = (filteredItems: TableItem<GlossaryItem>[]) => {
// Trigger pagination to update the number of buttons/pages due to filtering
totalRows.value = filteredItems.length
currentPage.value = 1
}
const fields: Exclude<TableFieldRaw<GlossaryItem>, string>[] = [
{
key: 'title',
sortable: true,
},
{
key: 'description',
sortable: true,
},
{
key: 'source',
sortable: true,
},
{
key: 'actions',
label: '',
},
]
onMounted(async () => {
isLoading.value = true
// TODO add APi call here
glossaryEntries.value = [
{
uuid: 'trzsdgres4',
title: 'Example 1',
description: 'Description for Exmaple 1.',
source: 'https://ccascoe.org',
},
{
uuid: 'nhtsgrzg4y',
title: 'Example 2',
description: 'Description for Exmaple 2.',
source: 'https://ccascoe.org',
},
{
uuid: 'gnrdu908ty945e',
title: 'Example 3',
description: 'Description for Exmaple 3.',
source: 'https://ccascoe.org',
},
{
uuid: 'u67ej7tewu',
title: 'Example f453q',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
{
uuid: 'y456wj64w',
title: 'Example htgxdfy54',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
{
uuid: '654w',
title: 'Example f4j76ro36753q',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
{
uuid: 'y563je',
title: 'Example gfdj67',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
{
uuid: 'jkuy09o7ie',
title: 'Example f4fgewar3 53q',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
{
uuid: '98o8fjgh',
title: 'Example vfaf321',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
{
uuid: '5y4286',
title: 'Example vgdfsaghtsh',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
{
uuid: 'hj7649i',
title: 'Example grayh4',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
{
uuid: 'j79',
title: 'Example jyayf34',
description: 'Description for Exmaple.',
source: 'https://ccascoe.org',
},
]
// TODO remove fake delay
await new Promise((resolve) => setTimeout(resolve, 1500));
isLoading.value = false
totalRows.value = glossaryEntries.value.length
})
</script>
<template>
<BContainer>
<BRow>
<BCol
lg="6"
class="my-1"
>
<BFormGroup
label="Filter"
label-for="filter-input"
label-cols-sm="3"
label-align-sm="right"
label-size="sm"
class="mb-0"
>
<BInputGroup size="sm">
<BFormInput
id="filter-input"
v-model="filter"
type="search"
placeholder="Type to Search"
/>
<BInputGroupText>
<BButton
:disabled="!filter"
@click="filter = ''"
>Clear</BButton
>
</BInputGroupText>
</BInputGroup>
</BFormGroup>
</BCol>
</BRow>
<BTable
sticky-header
striped
hover
bordered
:sort-by="[{ key: 'title', order: 'asc' }]"
:filter="filter"
:filter-function="filterFuntion"
@filtered="onFiltered"
:current-page="currentPage"
:per-page="perPage"
:busy="isLoading"
variant="primary"
:items="glossaryEntries"
:fields="fields"
>
<template #cell(actions)="row">
<BButton size="sm" @click="goToEdit(row.item, row.index)">{{
t('table.edit')
}}</BButton>
</template>
<template #table-busy>
<div class="text-center text-primary my-2">
<BSpinner variant="primary" class="align-middle" />
<strong class="ms-2">Loading...</strong>
</div>
</template>
</BTable>
<BRow>
<BCol
sm="7"
md="6"
class="my-1"
>
<BPagination
v-model="currentPage"
:total-rows="totalRows"
:per-page="perPage"
:align="'fill'"
size="sm"
class="my-0"
/>
</BCol>
<BCol
sm="5"
md="6"
class="my-1"
>
<BFormGroup
label="Per page"
label-for="per-page-select"
label-cols-sm="6"
label-cols-md="4"
label-cols-lg="3"
label-align-sm="right"
label-size="sm"
class="mb-0"
>
<BFormSelect
id="per-page-select"
v-model="perPage"
:options="pageOptions"
size="sm"
/>
</BFormGroup>
</BCol>
</BRow>
</BContainer>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { useGlossaryEntryStore } from '@/stores/glossaryEntry'
import type { GlossaryItem } from '@/types/GlossaryItem'
import { BButton, BCol, BContainer, BForm, BFormFloatingLabel, BFormGroup, BFormInput, BRow } from 'bootstrap-vue-next'
import { nextTick, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
const glossaryEntryStore = useGlossaryEntryStore()
const router = useRouter()
// use global scope
const { t } = useI18n({
useScope: 'global',
inheritLocale: true,
})
const form = reactive<GlossaryItem>({
uuid: glossaryEntryStore.glossaryEntry.uuid,
title: glossaryEntryStore.glossaryEntry.title,
description: glossaryEntryStore.glossaryEntry.description,
source: glossaryEntryStore.glossaryEntry.source,
})
const show = ref(true)
const saveSuccess = ref(false)
const toListView = () => {
router.push({ path: '/list' })
}
const onSubmit = (event: Event) => {
event.preventDefault()
//on save:
// 1. send update API call to server
// 2. Wait for success response
saveSuccess.value = true
// 3. router push back to list
// router.push({ path: '/list' })
}
/**
* Resets the values of the form back to empty strings
* @param event
*/
const onReset = (event: Event) => {
event.preventDefault()
// Reset our form values
// form.uuid = ''
form.title = ''
form.description = ''
form.source = ''
// Trick to reset/clear native browser form validation state
show.value = false
nextTick(() => {
show.value = true
})
}
</script>
<template>
<BContainer>
<BForm v-if="show" @submit="onSubmit" @reset="onReset">
<BFormGroup id="input-group-1" :label="t('editForm.title')" label-for="input-1">
<BFormInput
id="input-1"
v-model="form.title"
type="text"
:placeholder="t('editForm.titlePlaceholder')"
required
/>
</BFormGroup>
<BFormFloatingLabel id="input-group-2" :label="t('editForm.description')" label-for="input-2" class="my-2">
<BFormInput
id="input-2"
v-model="form.description"
type="text"
:placeholder="t('editForm.descriptionPlaceholder')"
required
/>
</BFormFloatingLabel>
<BFormGroup
id="input-group-3"
:label="t('editForm.source')"
label-for="input-3"
:description="t('editForm.sourceDescription')"
>
<BFormInput
id="input-3"
v-model="form.source"
type="url"
:placeholder="t('editForm.sourcePlaceholder')"
required
/>
</BFormGroup>
<BContainer>
<BRow>
<BCol>
<BButton type="submit" variant="primary">{{ t('editForm.submit') }}</BButton>
<BButton type="reset" variant="danger">{{ t('editForm.reset') }}</BButton>
</BCol>
<BCol>
<BButton variant="secondary" @click="toListView">{{ t('editForm.back') }}</BButton>
</BCol>
</BRow>
</BContainer>
</BForm>
<BContainer v-if="saveSuccess">
Entry Saved!
</BContainer>
</BContainer>
</template>
<style lang="css" scoped></style>

View File

@@ -1,11 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { BContainer } from 'bootstrap-vue-next'; import { BContainer } from 'bootstrap-vue-next'
</script> </script>
<template> <template>
<!-- <div class="bg-dark text-white text-center py-3 fixed-bottom">Footer</div> --> <!-- <div class="bg-dark text-white text-center py-3 fixed-bottom">Footer</div> -->
<BContainer fluid class="bg-dark text-white fixed-bottom py-3"> <BContainer fluid class="bg-dark text-white fixed-bottom py-3"> Footer </BContainer>
Footer
</BContainer>
</template> </template>

View File

@@ -9,7 +9,7 @@ import {
BNavbarToggle, BNavbarToggle,
BNavItem, BNavItem,
BNavItemDropdown, BNavItemDropdown,
vBColorMode vBColorMode,
} from 'bootstrap-vue-next' } from 'bootstrap-vue-next'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'

View File

@@ -8,5 +8,20 @@
"language": "Language", "language": "Language",
"logout": "logout", "logout": "logout",
"login": "login" "login": "login"
},
"table": {
"edit": "Edit"
},
"editForm": {
"title": "Title",
"titlePlaceholder": "Enter title",
"description": "Description",
"descriptionPlaceholder": "Enter description",
"source": "Source",
"sourcePlaceholder": "Enter source URL",
"sourceDescription": "Ensure the source is a valid URL.",
"submit": "Submit",
"reset": "Reset",
"back": "Back"
} }
} }

View File

@@ -8,5 +8,20 @@
"language": "Language", "language": "Language",
"logout": "se déconnecter", "logout": "se déconnecter",
"login": "se connecter" "login": "se connecter"
},
"table": {
"edit": "Modifier "
},
"editForm": {
"title": "Titre",
"titlePlaceholder": "Entrez le titre",
"description": "Description",
"descriptionPlaceholder": "Entrer la description",
"source": "Source",
"sourcePlaceholder": "Entrer l'URL source",
"sourceDescription": "Assurez-vous que la source est une URL valide.",
"submit": "Soumettre",
"reset": "Réinitialiser",
"back": "Retour"
} }
} }

View File

@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue' import Home from '@/views/Home.vue'
import List from '@/views/List.vue' import List from '@/views/List.vue'
import Edit from '@/views/Edit.vue'
import DefaultLayout from '@/layout/DefaultLayout.vue' import DefaultLayout from '@/layout/DefaultLayout.vue'
@@ -22,6 +23,11 @@ const router = createRouter({
name: 'List', name: 'List',
component: List, component: List,
}, },
{
path: '/edit',
name: 'Edit',
component: Edit,
},
], ],
}, },
], ],

View File

@@ -1,12 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,21 @@
import { ref, computed, type Ref } from 'vue'
import { defineStore } from 'pinia'
import type { GlossaryItem } from '@/types/GlossaryItem'
export const useGlossaryEntryStore = defineStore('glossaryEntry', () => {
const glossaryEntry: Ref<GlossaryItem> = ref({
uuid: '',
title: '',
description: '',
source: '',
})
const setGlossaryEntry = (passedGlossaryEntry: GlossaryItem) => {
glossaryEntry.value.uuid = passedGlossaryEntry.uuid
glossaryEntry.value.title = passedGlossaryEntry.title
glossaryEntry.value.description = passedGlossaryEntry.description
glossaryEntry.value.source = passedGlossaryEntry.source
}
return { glossaryEntry, setGlossaryEntry }
})

View File

@@ -0,0 +1,6 @@
export type GlossaryItem = {
uuid: string
title: string
description: string
source: string
}

12
src/views/Edit.vue Normal file
View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import AppEntryEditor from '@/components/AppEntryEditor.vue'
</script>
<template>
<div>
Edit view
<AppEntryEditor />
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import AppEntriesTable from '@/components/AppEntriesTable.vue'
</script> </script>
<template> <template>
<div>This is List</div> <div>This is List</div>
<AppEntriesTable />
</template> </template>
<style lang="css" scoped></style> <style lang="css" scoped></style>

15
src/vue-i18n.d.ts vendored
View File

@@ -21,6 +21,21 @@ declare module 'vue-i18n' {
logout: string logout: string
login: string login: string
} }
table: {
edit: string
}
editForm: {
title: string
titlePlaceholder: stirng
description: string
descriptionPlaceholder: string
source: string
sourcePlaceholder: string
sourceDescription: string
submit: string
reset: string
back: string
}
} }
// // define the datetime format schema // // define the datetime format schema