diff --git a/src/assets/images/Search.png b/src/assets/images/Search.png deleted file mode 100644 index 636ae765671e444b7430fdca5e18f74133318be9..0000000000000000000000000000000000000000 Binary files a/src/assets/images/Search.png and /dev/null differ diff --git a/src/modules/search/SearchModule.vue b/src/modules/search/SearchModule.vue index 5768713a9d548e91ad3a00c4a2aabc84e2de1ffb..06ba839a18906341c3ef9147d9037da75488f1b4 100644 --- a/src/modules/search/SearchModule.vue +++ b/src/modules/search/SearchModule.vue @@ -36,9 +36,9 @@ export default defineComponent({ async initialize() { // do initialization stuff (e.g. API calls, element loading, etc.) // ... + this.searchStore.retrieveSearchResults(); }, }, }); </script> -<style></style> diff --git a/src/modules/search/assets/dummy_data.ts b/src/modules/search/assets/dummy_data.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4888a5a445ca58f25c3738d25af1a69cc6fb5c2 --- /dev/null +++ b/src/modules/search/assets/dummy_data.ts @@ -0,0 +1,329 @@ +import { DummyDataEntry, DummyDataType } from "../types"; + +export const mainViewData: Array<DummyDataType> = [ + { + header: [ + { + type: "Project", + value: "SomeExampleProjectName1", + }, + ], + body: [ + { + type: "Created By", + value: "Max Mustermann", + }, + { + type: "Parent Project", + value: "None", + }, + { + type: "Start Date", + value: "01.01.2020", + }, + { + type: "End Date", + value: "01.12.2022", + }, + { + type: "PI", + value: "Prof. Müller", + }, + { + type: "Grant Id", + value: "DFG 1866", + }, + ], + }, + { + header: [ + { + type: "Project", + value: "SomeExampleProjectName2", + }, + ], + body: [ + { + type: "Created By", + value: "Manuel Rosenberger", + }, + { + type: "Parent Project", + value: "None", + }, + { + type: "Start Date", + value: "17.07.2020", + }, + { + type: "End Date", + value: "-", + }, + { + type: "PI", + value: "Prof. Müller", + }, + { + type: "Grant Id", + value: "DFG 7753", + }, + ], + }, + { + header: [ + { + type: "Project", + value: "SomeExampleProjectName3", + }, + ], + body: [ + { + type: "Created By", + value: "Hildegard Beck", + }, + { + type: "Parent Project", + value: "SomeExampleProjectName1", + }, + { + type: "Start Date", + value: "01.01.2020", + }, + { + type: "End Date", + value: "01.12.2022", + }, + { + type: "PI", + value: "Prof. Müller", + }, + { + type: "Grant Id", + value: "DFG 1867", + }, + ], + }, + { + header: [ + { + type: "Resource", + value: "SomeExampleResourceName", + archived: true, + } as DummyDataEntry, + ], + body: [ + { + type: "Persistent ID", + value: "21.11102/0372a8a6-3f4f-448e-aa75-acb75-acb148fe66d6", + }, + { + type: "License", + value: "MIT", + }, + { + type: "Usage Rights", + value: "None", + }, + { + type: "Application Profile", + value: "Radar", + }, + ], + }, + { + header: [ + { + type: "Resource", + value: "Test-Resource", + }, + ], + body: [ + { + type: "Persistent ID", + value: "21.11102/cf39b3e6-e0b7-4415-bc65-acb75-fbbbc35b4b89", + }, + { + type: "License", + value: "MIT", + }, + { + type: "Usage Rights", + value: "None", + }, + { + type: "Application Profile", + value: "Engmeta", + }, + ], + }, + { + header: [ + { + type: "Resource", + value: "Epic Project Resource", + archived: true, + } as DummyDataEntry, + ], + body: [ + { + type: "Persistent ID", + value: "21.11102/2a108fee-d60a-40e8-809a-acb75-8cf5aecb93f9", + }, + { + type: "License", + value: "MIT", + }, + { + type: "Usage Rights", + value: "None", + }, + { + type: "Application Profile", + value: "Radar", + }, + ], + }, + { + header: [ + { + type: "Resource", + value: "Epic Project Resource 2", + }, + ], + body: [ + { + type: "Persistent ID", + value: "21.11102/3f1bb053-644b-42c0-8e45-acb75-40193cde1a14", + }, + { + type: "License", + value: "MIT", + }, + { + type: "Usage Rights", + value: "None", + }, + { + type: "Application Profile", + value: "Radar", + }, + ], + }, + { + header: [ + { + type: "File", + value: "LabNotes.txt", + }, + ], + body: [ + { + type: "MD1", + value: "xxxxxxx", + }, + { + type: "MD2", + value: "yyyyyyyy", + }, + { + type: "MD3", + value: "None", + }, + { + type: "MD4", + value: "wwwwwww", + }, + { + type: "MD5", + value: "yyyyyyyyyyy", + }, + { + type: "MD6", + value: "None", + }, + { + type: "MD7", + value: "wwwwww", + }, + { + type: "MD8", + value: "yyyyyyyyyyyyy", + }, + { + type: "MD9", + value: "None", + }, + { + type: "MD10", + value: "wwwwwwww", + }, + { + type: "MD11", + value: "yyyyyyyyyyy", + }, + { + type: "MD12", + value: "None", + }, + { + type: "MD13", + value: "wwwwww", + }, + { + type: "MD14", + value: "yyyyyyyyy", + }, + { + type: "MD15", + value: "None", + }, + { + type: "MD16", + value: "wwwwwwww", + }, + ], + }, + { + header: [ + { + type: "User", + value: "Max Mustermann", + }, + ], + body: [ + { + type: "Project Member", + value: + "SomeExampleProjectName1, SomeExampleProjectName2, SomeExampleProjectName3", + }, + ], + }, + { + header: [ + { + type: "User", + value: "Manuel Rosenberger", + }, + ], + body: [ + { + type: "Project Member", + value: "SomeExampleProjectName2", + }, + ], + }, + { + header: [ + { + type: "User", + value: "Hildegard Beck", + }, + ], + body: [ + { + type: "Project Member", + value: "SomeExampleProjectName1, SomeExampleProjectName3", + }, + ], + }, +]; diff --git a/src/modules/search/i18n/de.ts b/src/modules/search/i18n/de.ts index 4a52e53fd70f89344c6cf3512e4f30665581e34f..6f0f1b1108c0a6521960f027d04c54e6b0fcd422 100644 --- a/src/modules/search/i18n/de.ts +++ b/src/modules/search/i18n/de.ts @@ -9,7 +9,18 @@ export default { page: { search: { title: "Suchseite", - description: "Das ist die @:page.search.title des Coscine UIv2 Apps", + search: "Suchen", + + buttonSearch: { + Item1: "Eintrag 1", + Item2: "Eintrag 2" + }, + + emptySearch: "Es wurden keine Treffer gefunden", + endSearchResults: "Alle Ergebnisse werden angezeigt", + + allProjects: "Alle Projekte", + allResources: "Alle Resourcen", }, }, } as VueI18n.LocaleMessageObject; diff --git a/src/modules/search/i18n/en.ts b/src/modules/search/i18n/en.ts index 718a6d258a9ba4894f7ed32a67d265b8c3f8514b..2e88e7fbbdb7c9efce36b11ecf15ec9ac8b82ce2 100644 --- a/src/modules/search/i18n/en.ts +++ b/src/modules/search/i18n/en.ts @@ -9,7 +9,18 @@ export default { page: { search: { title: "Search Page", - description: "This is the @:page.search.title for the Coscine UIv2 App", + search: "Search", + + buttonSearch: { + Item1: "Item1", + Item2: "Item2" + }, + + emptySearch: "No item is found for the given search criteria", + endSearchResults: "Showing all results", + + allProjects: "All Projects", + allResources: "All Resources", }, }, -} as VueI18n.LocaleMessageObject; +} as VueI18n.LocaleMessageObject; \ No newline at end of file diff --git a/src/modules/search/pages/Search.vue b/src/modules/search/pages/Search.vue index a2b912ba28e7c6d1d2ef38e6863c768d0d6a05fe..741420688225679e16b5e09562decfb32d04f3b3 100644 --- a/src/modules/search/pages/Search.vue +++ b/src/modules/search/pages/Search.vue @@ -1,28 +1,186 @@ <template> - <div> - <section - class="container flex flex-col items-center px-5 py-12 mx-auto text-gray-600 body-font md:flex-row" - > - <div> - <CoscineHeadline :headline="$parent.$t('page.search.title')" /> - <p class="mb-8 leading-relaxed dark:text-white"> - {{ $parent.$t("page.search.description") }} - </p> - <img alt="From Coscine Old" src="@/assets/images/Search.png" /> - </div> - </section> + <div class="search"> + <CoscineHeadline :headline="$parent.$t('page.search.title')" /> + + <b-row id="mainRow"> + <!-- Sidebar --> + <Sidebar /> + + <b-col ref="rightCol" sm="10" align-self="end" style="height: 100%"> + <!-- Search Bar Fields --> + <b-row id="searchBarContainer" align-content="center"> + <b-col id="searchField" align-self="start" class="pl-0"> + <b-form-input + v-model="searchText" + :placeholder="$parent.$t('page.search.search')" + ></b-form-input> + </b-col> + <b-col sm="2" id="selectProjCol" align-self="center" class="pl-0"> + <b-form-select v-model="selectProjValue"> + <template #first> + <b-form-select-option :value="null" disabled + >{{ $parent.$t("page.search.allProjects") }} + </b-form-select-option> + </template> + </b-form-select> + </b-col> + <b-col sm="2" id="selectResCol" align-self="center" class="pl-0"> + <b-form-select v-model="selectResValue"> + <template #first> + <b-form-select-option :value="null" disabled + >{{ $parent.$t("page.search.allResources") }} + </b-form-select-option> + </template> + </b-form-select> + </b-col> + <b-col sm="0" align-self="center" class="text-right p-0"> + <b-button-group> + <b-button id="searchButton" variant="primary"> + {{ $parent.$t("page.search.search") }} + </b-button> + <b-dropdown id="searchDropdown" right size="sm" variant="primary"> + <b-dropdown-item>{{ + $parent.$t("page.search.buttonSearch.Item1") + }}</b-dropdown-item> + <b-dropdown-item>{{ + $parent.$t("page.search.buttonSearch.Item2") + }}</b-dropdown-item> + </b-dropdown> + </b-button-group> + </b-col> + </b-row> + + <!-- Filter Tags --> + <b-row + id="filterTagsContainer" + align-self="center" + class="mt-2 mb-2 rounded bg-light" + style="min-height: 37px" + > + <b-col align-self="center" class="pl-0"> + <b-form-tags + v-model="filterTags" + no-outer-focus + class="border-0 bg-transparent" + > + <template v-slot="{ tags, removeTag }"> + <div> + <b-form-tag + v-for="tag in tags" + @remove="removeTag(tag)" + :key="tag" + :title="tag" + variant="primary" + pill + class="mr-1" + >{{ tag }} + </b-form-tag> + </div> + </template> + </b-form-tags> + </b-col> + <b-col align-self="center" sm="1" class="text-right"> + <b-button id="filterTagsButton" size="sm" variant="light"> + <b-icon icon="funnel-fill" /> + </b-button> + </b-col> + </b-row> + + <!-- Results View --> + <b-row id="resultsViewContainer" class="flex-grow-1"> + <b-card style="width: 100%"> + <b-skeleton-wrapper :loading="resultsViewLoading"> + <template #loading> + <div + v-for="(entry, index) in 3" + :key="index" + class="p-2 border-top" + style="height: 105px" + > + <b-skeleton width="30%" class="m-2 mb-3"></b-skeleton> + <b-skeleton width="95%" class="m-2"></b-skeleton> + <b-skeleton width="40%" class="m-2"></b-skeleton> + </div> + </template> + <b-table + id="resultsView" + :items="resultsViewData" + :fields="resultsViewFields" + :per-page="paginationPerPage" + :current-page="paginationCurrentPage" + thead-class="d-none" + style="min-height: 100%" + small + hover + sticky-header + show-empty + > + <template #cell(header)="data"> + <Result :result="data.item" /> + </template> + + <template #empty> + <h6 class="text-center"> + {{ $parent.$t("page.search.emptySearch") }} + </h6> + </template> + + <template #custom-foot="foot"> + <!-- Show footer if there are results and only on the last page --> + <div + v-if=" + foot.items.length > 0 && + paginationCurrentPage === + Math.ceil(paginationTotalRows / paginationPerPage) + " + class="p-2 text-center text-muted border-top" + > + {{ $parent.$t("page.search.endSearchResults") }} + </div> + </template> + </b-table> + </b-skeleton-wrapper> + </b-card> + </b-row> + </b-col> + </b-row> + + <!-- Pagination --> + <b-row class="mt-1 mb-1 text-right" align-v="center"> + <b-col align-self="center" class="p-0" /> + <b-col align-self="center" class="p-0"> + <b-pagination + id="pagination" + v-model="paginationCurrentPage" + :total-rows="paginationTotalRows" + :per-page="paginationPerPage" + aria-controls="resultsView" + align="center" + ></b-pagination> + </b-col> + <b-col align-self="center" class="p-0"> + <b-form-select + v-model="paginationPerPage" + :options="paginationPerPageOptions" + style="max-width: 5rem" + ></b-form-select> + </b-col> + </b-row> </div> </template> <script lang="ts"> import { defineComponent } from "vue-demi"; -import CoscineHeadline from "@/components/CoscineHeadline.vue"; +import Result from "./components/Result.vue"; +import Sidebar from "./components/Sidebar.vue"; // import the store for current module import { useSearchStore } from "../store"; // import the main store import { useMainStore } from "@/store/index"; +import type { DummyDataType } from "../types"; + export default defineComponent({ setup() { const mainStore = useMainStore(); @@ -31,8 +189,84 @@ export default defineComponent({ return { mainStore, searchStore }; }, + data() { + return { + searchText: "", + selectProjValue: null, + selectResValue: null, + + filterTags: ["Filter 1", "Filter 2", "Filter 3"], + + resultsViewFields: ["header"], + resultsViewLoading: true, + + paginationCurrentPage: 1, + paginationPerPage: 10, + paginationPerPageOptions: [5, 10, 20, 50, 100], + paginationTotalRows: 0, + }; + }, + components: { - CoscineHeadline, + Result, + Sidebar, + }, + + computed: { + resultsViewData(): DummyDataType[] | null { + return this.searchStore.searchResults; + }, + }, + + watch: { + resultsViewData() { + if (this.resultsViewData) { + this.paginationTotalRows = this.resultsViewData.length; + } + }, + }, + + created() { + this.showResults(); + }, + + methods: { + /* --- get search query to show results --- + getSearchQuery() { + const urlSearchParams = new URLSearchParams(window.location.search); + const query = Object.fromEntries(urlSearchParams.entries()); + return query !== null ? decodeURIComponent(query.q) : ""; + }, + */ + showResults() { + setTimeout(() => { + this.resultsViewLoading = false; + }, 1500); + }, }, }); </script> + +<style scoped> +#mainRow { + /* this style stretches the page vertically to fit the screen:-moz-animation: + - container-fluid <-> top = 59px + - mainRow <-> container-fluid = 53px + - mainRow <-> bottom = 62px + */ + height: calc(100vh - 59px - 53px - 62px); +} +#pagination { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} +#resultsViewContainer { + /* this style stretches the results table vertically + - resultsViewContainer <-> mainRow = 91px + */ + height: calc(100% - 91px); +} +.card-body { + padding: 0rem; +} +</style> diff --git a/src/modules/search/pages/components/Result.vue b/src/modules/search/pages/components/Result.vue new file mode 100644 index 0000000000000000000000000000000000000000..44a19436e8561813a5673f2e329676e1af08fc55 --- /dev/null +++ b/src/modules/search/pages/components/Result.vue @@ -0,0 +1,62 @@ +<template> + <div class="p-2"> + <a href="#"> + <div + id="resultHeader" + v-for="(header, index) in result.header" + :key="index" + class="mb-2 text-left text-primary" + > + <b>{{ header.type }}</b + >: {{ header.value }} + <b-badge v-if="header.archived" pill variant="warning" class="ml-1">{{ + $t("default.archived") + }}</b-badge> + </div> + </a> + + <div id="resultBody" class="text-left"> + <span + v-for="(element, index) in result.body" + :key="index" + class="mr-3 d-inline-block" + > + <!-- Keep the <br/> element at the end to have + double mouse click text selection work properly and + not have a table horizontal scrollbar appear --> + <b>{{ element.type }}</b + >: {{ element.value }}<br /> + </span> + </div> + </div> +</template> + +<script lang="ts"> +import { defineComponent, PropType } from "vue-demi"; +import { DummyDataType } from "../../types"; + +// import the store for current module +import { useSearchStore } from "../../store"; +// import the main store +import { useMainStore } from "@/store/index"; + + +export default defineComponent({ + setup() { + const mainStore = useMainStore(); + const searchStore = useSearchStore(); + return { mainStore, searchStore }; + }, + data() { + return {}; + }, + props: { + result: { + required: true, + type: Object as PropType<DummyDataType>, + }, + }, + methods: {}, +}); +</script> + diff --git a/src/modules/search/pages/components/Sidebar.vue b/src/modules/search/pages/components/Sidebar.vue new file mode 100644 index 0000000000000000000000000000000000000000..eecf99799713fd4490d01f6609c16ac66e4b821f --- /dev/null +++ b/src/modules/search/pages/components/Sidebar.vue @@ -0,0 +1,34 @@ +<template> + <b-col> + <b-card + id="sidebarContainer" + class="progress-bar-striped text-center bg-light" + style="height: 100%" + > + <!-- Sidebar components come here --> + </b-card> + </b-col> +</template> + +<script lang="ts"> +import { defineComponent } from "vue-demi"; + +// import the store for current module +import { useSearchStore } from "../../store"; +// import the main store +import { useMainStore } from "@/store/index"; + +export default defineComponent({ + setup() { + const mainStore = useMainStore(); + const searchStore = useSearchStore(); + return { mainStore, searchStore }; + }, +}); +</script> + +<style scoped> +.card-body { + padding: 0rem; +} +</style> diff --git a/src/modules/search/store.ts b/src/modules/search/store.ts index 09d81245af8f4f554a4286b4c61651105b244fa4..6c0f0681971ddc4b4298b5876f0238dccdd8282b 100644 --- a/src/modules/search/store.ts +++ b/src/modules/search/store.ts @@ -1,5 +1,8 @@ import { defineStore } from "pinia"; import { SearchState } from "./types"; +import { mainViewData } from "./assets/dummy_data"; + +import type { DummyDataType } from "./types"; /* Store variable name is "this.<id>Store" @@ -13,7 +16,9 @@ export const useSearchStore = defineStore({ STATES -------------------------------------------------------------------------------------- */ - state: (): SearchState => ({}), + state: (): SearchState => ({ + searchResults: null, + }), /* -------------------------------------------------------------------------------------- @@ -24,7 +29,11 @@ export const useSearchStore = defineStore({ In a component use as e.g.: :label = "this.searchStore.<getter_name>; */ - getters: {}, + getters: { + retrieveDummyResults(): DummyDataType[] { + return mainViewData; + } + }, /* -------------------------------------------------------------------------------------- ACTIONS @@ -35,7 +44,12 @@ export const useSearchStore = defineStore({ In a component use as e.g.: @click = "this.searchStore.<action_name>(); */ - actions: {}, + actions: { + retrieveSearchResults() { + // Currently using only Dummy Data + this.searchResults = this.retrieveDummyResults; + } + }, }); export default useSearchStore; diff --git a/src/modules/search/types.ts b/src/modules/search/types.ts index 0b1741214fd4d49f59618332118c8ab9009e5d2c..598b0ce804ddd274f0b079d608eea23f68afb7da 100644 --- a/src/modules/search/types.ts +++ b/src/modules/search/types.ts @@ -1,7 +1,22 @@ +import type { SearchResult } from "@coscine/api-client/dist/types/Coscine.Api.Search"; + export interface SearchState { /* -------------------------------------------------------------------------------------- STATE TYPE DEFINITION -------------------------------------------------------------------------------------- */ + searchResults: DummyDataType[] | null, // Fix type for real API use } + +/* DELETE AFTER DATA IS ACTUALLY FETCHED OVER THE API */ +export type DummyDataType = { + header: Array<DummyDataEntry>; + body: Array<DummyDataEntry>; +}; + +/* DELETE AFTER DATA IS ACTUALLY FETCHED OVER THE API */ +export type DummyDataEntry = { + type: string; + value: string; +};