@fy-/fws-vue 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/fws/CmsArticleBoxed.vue +113 -0
- package/components/fws/CmsArticleSingle.vue +115 -0
- package/components/fws/DataTable.vue +260 -0
- package/components/fws/FilterData.vue +179 -0
- package/components/fws/UserFlow.vue +305 -0
- package/components/ssr/ClientOnly.ts +12 -0
- package/components/ui/DefaultBreadcrumb.vue +75 -0
- package/components/ui/DefaultConfirm.vue +69 -0
- package/components/ui/DefaultDateSelection.vue +56 -0
- package/components/ui/DefaultInput.vue +243 -0
- package/components/ui/DefaultLoader.vue +49 -0
- package/components/ui/DefaultModal.vue +90 -0
- package/components/ui/DefaultPaging.vue +212 -0
- package/components/ui/transitions/CollapseTransition.vue +30 -0
- package/components/ui/transitions/ExpandTransition.vue +32 -0
- package/components/ui/transitions/FadeTransition.vue +17 -0
- package/components/ui/transitions/ScaleTransition.vue +35 -0
- package/components/ui/transitions/SlideTransition.vue +127 -0
- package/components.d.ts +22 -0
- package/env.d.ts +6 -0
- package/event-bus.ts +14 -0
- package/index.ts +121 -0
- package/package.json +43 -0
- package/rest.ts +72 -0
- package/seo.ts +114 -0
- package/ssr.ts +97 -0
- package/stores/rest.ts +24 -0
- package/stores/serverRouter.ts +49 -0
- package/stores/user.ts +81 -0
- package/style.css +42 -0
- package/templating.ts +79 -0
- package/translations.ts +29 -0
- package/types.d.ts +4 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { CalendarDaysIcon } from "@heroicons/vue/24/solid";
|
|
3
|
+
|
|
4
|
+
defineProps({
|
|
5
|
+
article: {
|
|
6
|
+
type: Object,
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
type: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: "blog",
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<article
|
|
18
|
+
itemscope
|
|
19
|
+
itemtype="https://schema.org/Article"
|
|
20
|
+
class="p-3 flex flex-col justify-between bg-white noise rounded-lg border border-fv-neutral-200 shadow-md dark:bg-fv-neutral-800 dark:border-fv-neutral-700"
|
|
21
|
+
>
|
|
22
|
+
<meta itemprop="wordCount" :content="article.WordCount" />
|
|
23
|
+
<meta itemprop="datePublished" :content="article.CreatedAt.iso" />
|
|
24
|
+
<meta itemprop="dateModified" :content="article.UpdatedAt.iso" />
|
|
25
|
+
<meta itemprop="inLanguage" :content="article.Locale" />
|
|
26
|
+
<meta itemprop="headline" :content="article.Title" />
|
|
27
|
+
<meta
|
|
28
|
+
itemprop="thumbnailUrl"
|
|
29
|
+
v-if="article.CoverUUID"
|
|
30
|
+
:content="`https://s.nocachenocry.com/${article.CoverUUID}?vars=format=webp:resize=512x512`"
|
|
31
|
+
/>
|
|
32
|
+
<RouterLink
|
|
33
|
+
:to="`/${type}/${article.Slug}`"
|
|
34
|
+
:title="article.Title"
|
|
35
|
+
:alt="article.Title"
|
|
36
|
+
><img
|
|
37
|
+
v-if="article.CoverUUID"
|
|
38
|
+
:src="`https://s.nocachenocry.com/${article.CoverUUID}?vars=format=webp:scale_crop_center=400x195`"
|
|
39
|
+
loading="lazy"
|
|
40
|
+
:title="article.Title"
|
|
41
|
+
:alt="article.Title"
|
|
42
|
+
class="w-full rounded-lg bg-fv-neutral-800 shadow mb-3 flex-grow-0 flex-shrink-0"
|
|
43
|
+
width="400"
|
|
44
|
+
height="250"
|
|
45
|
+
/></RouterLink>
|
|
46
|
+
<h2 class="title-1 font-semibold mb-2 flex-grow-0">
|
|
47
|
+
<RouterLink
|
|
48
|
+
:to="`/${type}/${article.Slug}`"
|
|
49
|
+
:title="article.Title"
|
|
50
|
+
:alt="article.Title"
|
|
51
|
+
rel="bookmark"
|
|
52
|
+
>{{ article.Title }}</RouterLink
|
|
53
|
+
>
|
|
54
|
+
</h2>
|
|
55
|
+
<p
|
|
56
|
+
class="mb-5 font-light text-fv-neutral-500 dark:text-fv-neutral-400 flex-grow"
|
|
57
|
+
itemprop="description"
|
|
58
|
+
>
|
|
59
|
+
{{ article.Overview }}
|
|
60
|
+
</p>
|
|
61
|
+
<div class="flex justify-between items-center flex-grow-0">
|
|
62
|
+
<div class="flex justify-end items-center text-fv-neutral-500">
|
|
63
|
+
<time
|
|
64
|
+
class="text-sm inline-flex items-center justify-center gap-x-1"
|
|
65
|
+
itemprop="datePublished"
|
|
66
|
+
:content="new Date(parseInt(article.CreatedAt.unixms)).toISOString()"
|
|
67
|
+
:datetime="new Date(parseInt(article.CreatedAt.unixms)).toISOString()"
|
|
68
|
+
>
|
|
69
|
+
<CalendarDaysIcon class="w-4 h-4 -mt-0.5" />
|
|
70
|
+
{{ $formatDate(article.CreatedAt.unixms) }}
|
|
71
|
+
</time>
|
|
72
|
+
<meta
|
|
73
|
+
itemprop="dateModified"
|
|
74
|
+
:content="new Date(parseInt(article.UpdatedAt.unixms)).toISOString()"
|
|
75
|
+
/>
|
|
76
|
+
<meta itemprop="inLanguage" :content="article.Language__" />
|
|
77
|
+
<meta
|
|
78
|
+
itemprop="dateModified"
|
|
79
|
+
:content="new Date(parseInt(article.UpdatedAt.unixms)).toISOString()"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
<!--<span
|
|
83
|
+
><img
|
|
84
|
+
class="h-4 shadow"
|
|
85
|
+
:src="langs[article.Locale]"
|
|
86
|
+
width="24"
|
|
87
|
+
height="16"
|
|
88
|
+
loading="lazy"
|
|
89
|
+
/>
|
|
90
|
+
</span>-->
|
|
91
|
+
<RouterLink
|
|
92
|
+
:to="`/${type}/${article.Slug}`"
|
|
93
|
+
:title="article.Title"
|
|
94
|
+
:alt="article.Title"
|
|
95
|
+
class="inline-flex items-center font-medium text-primary-600 dark:text-primary-500 hover:underline"
|
|
96
|
+
>
|
|
97
|
+
{{ $t("read_more_cta") }}
|
|
98
|
+
<svg
|
|
99
|
+
class="ml-2 w-4 h-4"
|
|
100
|
+
fill="currentColor"
|
|
101
|
+
viewBox="0 0 20 20"
|
|
102
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
103
|
+
>
|
|
104
|
+
<path
|
|
105
|
+
fill-rule="evenodd"
|
|
106
|
+
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
|
|
107
|
+
clip-rule="evenodd"
|
|
108
|
+
></path>
|
|
109
|
+
</svg>
|
|
110
|
+
</RouterLink>
|
|
111
|
+
</div>
|
|
112
|
+
</article>
|
|
113
|
+
</template>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { useRoute } from "vue-router";
|
|
4
|
+
import type { Component } from "vue";
|
|
5
|
+
import { useRest } from "../../rest";
|
|
6
|
+
import { LazyHead, useSeo } from "../../seo";
|
|
7
|
+
import type { BreadcrumbLink } from "../../types";
|
|
8
|
+
import DefaultBreadcrumb from "../ui/DefaultBreadcrumb.vue";
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
cmsAlias: string;
|
|
14
|
+
notFound: Component;
|
|
15
|
+
baseBreadcrumb?: BreadcrumbLink[];
|
|
16
|
+
}>(),
|
|
17
|
+
{
|
|
18
|
+
baseBreadcrumb: () => [],
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const rest = useRest();
|
|
23
|
+
const post = ref<any>([]);
|
|
24
|
+
const route = useRoute();
|
|
25
|
+
const seo = ref<LazyHead>({});
|
|
26
|
+
const is404 = ref(false);
|
|
27
|
+
const getBlogPost = async () => {
|
|
28
|
+
const data = await rest(
|
|
29
|
+
`Cms/${props.cmsAlias}/Post/${route.params.slug}`,
|
|
30
|
+
"GET",
|
|
31
|
+
);
|
|
32
|
+
if (data && data.result == "success") {
|
|
33
|
+
post.value = data.data;
|
|
34
|
+
seo.value.title = post.value.Title;
|
|
35
|
+
seo.value.description = post.value.Overview;
|
|
36
|
+
|
|
37
|
+
if (post.value.CoverUUID) {
|
|
38
|
+
seo.value.image = `https://s.nocachenocry.com/${post.value.CoverUUID}?vars=format=png:resize=512x512`;
|
|
39
|
+
seo.value.imageWidth = "512";
|
|
40
|
+
seo.value.imageHeight = "512";
|
|
41
|
+
seo.value.imageType = "image/png";
|
|
42
|
+
}
|
|
43
|
+
if (post.value.Locale != "") {
|
|
44
|
+
seo.value.locale = post.value.Locale;
|
|
45
|
+
} else {
|
|
46
|
+
seo.value.locale = "en-US";
|
|
47
|
+
}
|
|
48
|
+
seo.value.canonical = `https://${props.baseUrl}/l/${seo.value.locale}/blog/${post.value.Slug}`;
|
|
49
|
+
seo.value.url = `https://${props.baseUrl}/l/${seo.value.locale}/blog/${post.value.Slug}`;
|
|
50
|
+
if (post.value.Locales && post.value.Locales.length > 1) {
|
|
51
|
+
seo.value.alternateLocales = post.value.Locales;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
is404.value = true;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
await getBlogPost();
|
|
58
|
+
useSeo(seo);
|
|
59
|
+
</script>
|
|
60
|
+
<template>
|
|
61
|
+
<div>
|
|
62
|
+
<div v-if="!is404 && post">
|
|
63
|
+
<div class="items-center flex justify-center mt-3">
|
|
64
|
+
<DefaultBreadcrumb
|
|
65
|
+
v-if="baseBreadcrumb.length > 0"
|
|
66
|
+
:show-home="false"
|
|
67
|
+
:nav="[...baseBreadcrumb, { name: post.Title }]"
|
|
68
|
+
class="!hidden md:!flex"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
<article
|
|
72
|
+
class="h-full flex flex-col scrollbar-hidden"
|
|
73
|
+
itemscope
|
|
74
|
+
itemtype="https://schema.org/Article"
|
|
75
|
+
>
|
|
76
|
+
<meta itemprop="wordCount" :content="post.WordCount" />
|
|
77
|
+
<meta itemprop="datePublished" :content="post.CreatedAt.iso" />
|
|
78
|
+
<meta itemprop="dateModified" :content="post.UpdatedAt.iso" />
|
|
79
|
+
<meta itemprop="inLanguage" :content="post.Locale" />
|
|
80
|
+
<meta itemprop="headline" :content="post.Title" />
|
|
81
|
+
<meta
|
|
82
|
+
itemprop="thumbnailUrl"
|
|
83
|
+
v-if="post.CoverUUID"
|
|
84
|
+
:content="`https://s.nocachenocry.com/${post.CoverUUID}?vars=format=webp:resize=512x512`"
|
|
85
|
+
/>
|
|
86
|
+
<div class="py-8 container xl:max-w-6xl mx-auto px-4">
|
|
87
|
+
<h2
|
|
88
|
+
class="mb-4 text-4xl tracking-tight font-extrabold text-center text-fv-neutral-900 dark:text-white"
|
|
89
|
+
>
|
|
90
|
+
{{ post.Title }}
|
|
91
|
+
</h2>
|
|
92
|
+
<p
|
|
93
|
+
class="font-light text-center text-fv-neutral-500 dark:text-fv-neutral-400 sm:text-xl"
|
|
94
|
+
>
|
|
95
|
+
{{ post.Overview }}
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
<img
|
|
99
|
+
v-if="post.CoverUUID"
|
|
100
|
+
:src="`https://s.nocachenocry.com/${post.CoverUUID}?vars=format=webp:resize=768x768`"
|
|
101
|
+
:alt="post.Title"
|
|
102
|
+
class="h-auto rounded-xl shadow max-w-[768px] mx-auto mb-6"
|
|
103
|
+
/>
|
|
104
|
+
<div class="page-clear-container relative mb-6">
|
|
105
|
+
<section
|
|
106
|
+
itemprop="articleBody"
|
|
107
|
+
class="prose dark:prose-invert max-w-6xl mx-auto"
|
|
108
|
+
v-html="post.Body"
|
|
109
|
+
></section>
|
|
110
|
+
</div>
|
|
111
|
+
</article>
|
|
112
|
+
</div>
|
|
113
|
+
<component :is="notFound" v-if="is404" />
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref, onUnmounted, watch } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
ArrowDownIcon,
|
|
5
|
+
ArrowUpIcon,
|
|
6
|
+
ArrowDownTrayIcon,
|
|
7
|
+
} from "@heroicons/vue/24/solid";
|
|
8
|
+
import DefaultPaging from "../ui/DefaultPaging.vue";
|
|
9
|
+
import DefaultInput from "../ui/DefaultInput.vue";
|
|
10
|
+
import { useEventBus } from "../../event-bus";
|
|
11
|
+
import { useRest } from "../../rest";
|
|
12
|
+
import { useRoute } from "vue-router";
|
|
13
|
+
import { useStorage } from "@vueuse/core";
|
|
14
|
+
interface DefaultStringObject {
|
|
15
|
+
[key: string]: string;
|
|
16
|
+
}
|
|
17
|
+
interface DefaultAnyObject {
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
interface DefaultBoolObject {
|
|
21
|
+
[key: string]: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface SortingField {
|
|
24
|
+
field: string;
|
|
25
|
+
direction: string;
|
|
26
|
+
}
|
|
27
|
+
const eventBus = useEventBus();
|
|
28
|
+
const currentPage = ref<number>(1);
|
|
29
|
+
const route = useRoute();
|
|
30
|
+
const data = ref<any[]>([]);
|
|
31
|
+
const paging = ref<any>(undefined);
|
|
32
|
+
const perPageOptions = [
|
|
33
|
+
["10", "10"],
|
|
34
|
+
["25", "25"],
|
|
35
|
+
["50", "50"],
|
|
36
|
+
["100", "100"],
|
|
37
|
+
];
|
|
38
|
+
const props = withDefaults(
|
|
39
|
+
defineProps<{
|
|
40
|
+
id: string;
|
|
41
|
+
headers: DefaultStringObject;
|
|
42
|
+
sortables?: DefaultBoolObject;
|
|
43
|
+
showHeaders?: boolean;
|
|
44
|
+
exportableColumns?: string[];
|
|
45
|
+
csvFormatColumns?: Record<string, (value: any) => string>;
|
|
46
|
+
defaultPerPage?: number;
|
|
47
|
+
filtersData: DefaultAnyObject;
|
|
48
|
+
apiPath: string;
|
|
49
|
+
defaultSort?: SortingField;
|
|
50
|
+
restFunction?: Function | null;
|
|
51
|
+
}>(),
|
|
52
|
+
{
|
|
53
|
+
showHeaders: true,
|
|
54
|
+
sortables: () => ({}),
|
|
55
|
+
exportableColumns: () => [],
|
|
56
|
+
csvFormatColumns: () => ({}),
|
|
57
|
+
exportableName: "default",
|
|
58
|
+
defaultPerPage: 25,
|
|
59
|
+
defaultSort: () => ({ field: "Created", direction: "DESC" }),
|
|
60
|
+
restFunction: null,
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
const rest = useRest();
|
|
64
|
+
const restFunction = props.restFunction ?? rest;
|
|
65
|
+
const perPage = useStorage<number>(`${props.id}PerPage`, props.defaultPerPage);
|
|
66
|
+
const currentSort = useStorage<SortingField>(
|
|
67
|
+
`${props.id}CurrentSort`,
|
|
68
|
+
props.defaultSort,
|
|
69
|
+
);
|
|
70
|
+
const getData = async (page: number = 1) => {
|
|
71
|
+
eventBus.emit("main-loading", true);
|
|
72
|
+
if (route.query.page) page = parseInt(route.query.page.toString());
|
|
73
|
+
const sort: any = {};
|
|
74
|
+
sort[currentSort.value.field] = currentSort.value.direction;
|
|
75
|
+
const requestParams = {
|
|
76
|
+
...props.filtersData,
|
|
77
|
+
sort: sort,
|
|
78
|
+
results_per_page: perPage.value,
|
|
79
|
+
page_no: page,
|
|
80
|
+
};
|
|
81
|
+
const r = await restFunction(props.apiPath, "GET", requestParams, {
|
|
82
|
+
getBody: true,
|
|
83
|
+
});
|
|
84
|
+
currentPage.value = page;
|
|
85
|
+
data.value = [];
|
|
86
|
+
paging.value = undefined;
|
|
87
|
+
if (r && r.result == "success") {
|
|
88
|
+
data.value = r.data;
|
|
89
|
+
paging.value = r.paging;
|
|
90
|
+
eventBus.emit(`${props.id}NewData`, data.value);
|
|
91
|
+
}
|
|
92
|
+
eventBus.emit("main-loading", false);
|
|
93
|
+
};
|
|
94
|
+
const sortData = (key: string) => {
|
|
95
|
+
if (!props.sortables[key]) return;
|
|
96
|
+
const newSort: SortingField = {
|
|
97
|
+
field: currentSort.value.field,
|
|
98
|
+
direction: currentSort.value.direction,
|
|
99
|
+
};
|
|
100
|
+
if (key == newSort.field) {
|
|
101
|
+
if (newSort.direction == "desc") {
|
|
102
|
+
newSort.direction = "asc";
|
|
103
|
+
} else {
|
|
104
|
+
newSort.direction = "desc";
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
newSort.direction = "desc";
|
|
108
|
+
newSort.field = key;
|
|
109
|
+
}
|
|
110
|
+
currentSort.value = { ...newSort };
|
|
111
|
+
};
|
|
112
|
+
const exportToCsv = () => {
|
|
113
|
+
const header = props.exportableColumns
|
|
114
|
+
.map((column) => props.headers[column] ?? column)
|
|
115
|
+
.join(",");
|
|
116
|
+
const rows = data.value
|
|
117
|
+
.map((row) => {
|
|
118
|
+
return props.exportableColumns
|
|
119
|
+
.map((column) => {
|
|
120
|
+
let cell = row[column];
|
|
121
|
+
if (props.csvFormatColumns[column]) {
|
|
122
|
+
cell = props.csvFormatColumns[column](row);
|
|
123
|
+
}
|
|
124
|
+
return `"${cell}"`;
|
|
125
|
+
})
|
|
126
|
+
.join(",");
|
|
127
|
+
})
|
|
128
|
+
.join("\n");
|
|
129
|
+
|
|
130
|
+
const csvContent = header + "\n" + rows;
|
|
131
|
+
|
|
132
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
133
|
+
const link = document.createElement("a");
|
|
134
|
+
const url = URL.createObjectURL(blob);
|
|
135
|
+
link.setAttribute("href", url);
|
|
136
|
+
link.setAttribute(
|
|
137
|
+
"download",
|
|
138
|
+
`${props.id}_${new Date().toISOString().slice(0, 10)}_Page-${
|
|
139
|
+
currentPage.value
|
|
140
|
+
}_${perPage.value}-per-page_Order-by-${currentSort.value.field}-${
|
|
141
|
+
currentSort.value.direction
|
|
142
|
+
}.csv`,
|
|
143
|
+
);
|
|
144
|
+
link.style.visibility = "hidden";
|
|
145
|
+
document.body.appendChild(link);
|
|
146
|
+
link.click();
|
|
147
|
+
document.body.removeChild(link);
|
|
148
|
+
};
|
|
149
|
+
watch(perPage, () => {
|
|
150
|
+
getData();
|
|
151
|
+
});
|
|
152
|
+
watch(currentSort, () => {
|
|
153
|
+
getData();
|
|
154
|
+
});
|
|
155
|
+
watch(
|
|
156
|
+
() => props.filtersData,
|
|
157
|
+
() => {
|
|
158
|
+
getData();
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
watch(
|
|
162
|
+
() => props.apiPath,
|
|
163
|
+
() => {
|
|
164
|
+
getData();
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
onMounted(() => {
|
|
168
|
+
eventBus.on(`${props.id}PagesGoToPage`, getData);
|
|
169
|
+
eventBus.on(`${props.id}Reload`, getData);
|
|
170
|
+
eventBus.on(`${props.id}Refresh`, getData);
|
|
171
|
+
|
|
172
|
+
getData();
|
|
173
|
+
});
|
|
174
|
+
onUnmounted(() => {
|
|
175
|
+
eventBus.off(`${props.id}PagesGoToPage`, getData);
|
|
176
|
+
eventBus.off(`${props.id}Reload`, getData);
|
|
177
|
+
eventBus.off(`${props.id}Refresh`, getData);
|
|
178
|
+
});
|
|
179
|
+
</script>
|
|
180
|
+
<template>
|
|
181
|
+
<div>
|
|
182
|
+
<div
|
|
183
|
+
class="flex gap-2 justify-between items-center border-b border-fv-primary-600 mb-2 pb-2"
|
|
184
|
+
>
|
|
185
|
+
<DefaultPaging :items="paging" v-if="paging" :id="`${props.id}Pages`" />
|
|
186
|
+
<button
|
|
187
|
+
class="btn primary defaults"
|
|
188
|
+
@click="exportToCsv"
|
|
189
|
+
v-if="exportableColumns.length && data.length"
|
|
190
|
+
>
|
|
191
|
+
<ArrowDownTrayIcon class="w-4 h-4 mr-2"></ArrowDownTrayIcon
|
|
192
|
+
>{{ $t("global_table_export") }}
|
|
193
|
+
</button>
|
|
194
|
+
<DefaultInput
|
|
195
|
+
v-model="perPage"
|
|
196
|
+
:options="perPageOptions"
|
|
197
|
+
:show-label="false"
|
|
198
|
+
:id="`${id}PerPage`"
|
|
199
|
+
type="select"
|
|
200
|
+
class="w-20"
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div
|
|
205
|
+
class="relative overflow-x-auto border-fv-primary-600 sm:rounded-lg"
|
|
206
|
+
v-if="data.length"
|
|
207
|
+
>
|
|
208
|
+
<table
|
|
209
|
+
class="w-full text-sm text-left text-fv-neutral-500 dark:text-fv-neutral-400"
|
|
210
|
+
>
|
|
211
|
+
<thead
|
|
212
|
+
v-if="showHeaders"
|
|
213
|
+
class="text-xs text-fv-neutral-700 uppercase bg-fv-neutral-50 dark:bg-fv-neutral-800 dark:text-fv-neutral-400"
|
|
214
|
+
>
|
|
215
|
+
<tr>
|
|
216
|
+
<th
|
|
217
|
+
v-for="(header, key) in headers"
|
|
218
|
+
:key="key"
|
|
219
|
+
@click="
|
|
220
|
+
() => {
|
|
221
|
+
if (sortables[key]) {
|
|
222
|
+
sortData(key.toString());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
"
|
|
226
|
+
scope="col"
|
|
227
|
+
class="px-6 py-3 whitespace-nowrap"
|
|
228
|
+
:class="{
|
|
229
|
+
'cursor-pointer': sortables[key],
|
|
230
|
+
}"
|
|
231
|
+
>
|
|
232
|
+
{{ header }}
|
|
233
|
+
<template v-if="sortables[key] && currentSort.field == key">
|
|
234
|
+
<ArrowUpIcon
|
|
235
|
+
v-if="currentSort.direction == 'desc'"
|
|
236
|
+
class="inline w-3 h-3 align-top mt-0.5"
|
|
237
|
+
/>
|
|
238
|
+
<ArrowDownIcon v-else class="inline w-3 h-3 align-top mt-0.5" />
|
|
239
|
+
</template>
|
|
240
|
+
</th>
|
|
241
|
+
</tr>
|
|
242
|
+
</thead>
|
|
243
|
+
<tbody>
|
|
244
|
+
<tr
|
|
245
|
+
v-for="(row, index) in data"
|
|
246
|
+
:key="index"
|
|
247
|
+
class="bg-white border-b dark:bg-fv-neutral-900 dark:border-fv-neutral-800 hover:bg-fv-neutral-50 dark:hover:bg-fv-neutral-950"
|
|
248
|
+
>
|
|
249
|
+
<td v-for="(header, key) in headers" :key="key" class="px-6 py-4">
|
|
250
|
+
<slot :name="key" :value="row">
|
|
251
|
+
<template v-if="row[key]">{{ row[key] }} </template>
|
|
252
|
+
<template v-else>{{ $t("global_table_empty_cell") }}</template>
|
|
253
|
+
</slot>
|
|
254
|
+
</td>
|
|
255
|
+
</tr>
|
|
256
|
+
</tbody>
|
|
257
|
+
</table>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</template>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import useVuelidate from "@vuelidate/core";
|
|
3
|
+
import { reactive } from "vue";
|
|
4
|
+
import { onMounted } from "vue";
|
|
5
|
+
import { onUnmounted } from "vue";
|
|
6
|
+
import DefaultInput from "../ui/DefaultInput.vue";
|
|
7
|
+
import { useTranslation } from "../../translations";
|
|
8
|
+
import { useEventBus } from "../../event-bus";
|
|
9
|
+
import DefaultDateSelection from "../ui/DefaultDateSelection.vue";
|
|
10
|
+
interface FilterData {
|
|
11
|
+
label: string;
|
|
12
|
+
req: boolean;
|
|
13
|
+
uid: string;
|
|
14
|
+
type: string;
|
|
15
|
+
restValue?: string;
|
|
16
|
+
options?: any[][];
|
|
17
|
+
isHidden?: boolean;
|
|
18
|
+
default?: any | undefined;
|
|
19
|
+
formats?: Record<string, (value: any) => any>;
|
|
20
|
+
formatRestValue?: (value: any) => any;
|
|
21
|
+
onChangeValue?: (form: any, value: any) => void;
|
|
22
|
+
}
|
|
23
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
24
|
+
const state = reactive<any>({ formData: {} });
|
|
25
|
+
const rules: any = { formData: {} };
|
|
26
|
+
const types = reactive<any>({});
|
|
27
|
+
const translate = useTranslation();
|
|
28
|
+
const props = withDefaults(
|
|
29
|
+
defineProps<{
|
|
30
|
+
data?: Array<Array<FilterData>>;
|
|
31
|
+
css: string;
|
|
32
|
+
modelValue?: Record<string, unknown>;
|
|
33
|
+
}>(),
|
|
34
|
+
{
|
|
35
|
+
showHeaders: true,
|
|
36
|
+
data: () => [],
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
const removeUndefinedStrings = (
|
|
40
|
+
input: any,
|
|
41
|
+
undefinedValues: any[] = ["undefined"],
|
|
42
|
+
) => {
|
|
43
|
+
const output: any = {};
|
|
44
|
+
|
|
45
|
+
Object.keys(input).forEach((key) => {
|
|
46
|
+
if (!undefinedValues.includes(input[key]) && input[key] !== undefined) {
|
|
47
|
+
if (!input[key]["$between"]) {
|
|
48
|
+
output[key] = input[key];
|
|
49
|
+
} else {
|
|
50
|
+
input[key]["$between"][0] =
|
|
51
|
+
input[key]["$between"][0] == "" || input[key]["$between"][0] == null
|
|
52
|
+
? undefined
|
|
53
|
+
: input[key]["$between"][0];
|
|
54
|
+
input[key]["$between"][1] =
|
|
55
|
+
input[key]["$between"][1] == "" || input[key]["$between"][1] == null
|
|
56
|
+
? undefined
|
|
57
|
+
: input[key]["$between"][1];
|
|
58
|
+
if (
|
|
59
|
+
input[key]["$between"][0] !== undefined ||
|
|
60
|
+
input[key]["$between"][1] !== undefined
|
|
61
|
+
) {
|
|
62
|
+
output[key] = input[key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return output;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const formatValues = (obj: any) => {
|
|
72
|
+
props.data.forEach((group) => {
|
|
73
|
+
group.forEach((f) => {
|
|
74
|
+
if (f.formats && f.formats[f.type]) {
|
|
75
|
+
obj[f.uid] = f.formats[f.type](obj[f.uid]);
|
|
76
|
+
}
|
|
77
|
+
if (f.formatRestValue) {
|
|
78
|
+
obj[f.uid] = f.formatRestValue(obj[f.uid]);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
return removeUndefinedStrings(obj, ["undefined", ""]);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const updateForms = () => {
|
|
86
|
+
state.formData = {};
|
|
87
|
+
rules.formData = {};
|
|
88
|
+
props.data.forEach((group) => {
|
|
89
|
+
group.forEach((f) => {
|
|
90
|
+
state.formData[f.uid] =
|
|
91
|
+
typeof f.default == "object" && f.default
|
|
92
|
+
? JSON.parse(JSON.stringify(f.default))
|
|
93
|
+
: f.default;
|
|
94
|
+
|
|
95
|
+
types[f.uid] = f.type;
|
|
96
|
+
|
|
97
|
+
if (f.options && f.options.length) {
|
|
98
|
+
f.options = f.options.map((status) => {
|
|
99
|
+
const [statusKey, statusValue] = status;
|
|
100
|
+
const translatedValue = translate(statusValue);
|
|
101
|
+
return [statusKey, translatedValue];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
rules.formData[f.uid] = {};
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
emit("update:modelValue", formatValues({ ...state.formData }));
|
|
108
|
+
};
|
|
109
|
+
updateForms();
|
|
110
|
+
const v$ = useVuelidate(rules, state);
|
|
111
|
+
|
|
112
|
+
const submitForm = () => {
|
|
113
|
+
emit("update:modelValue", formatValues({ ...state.formData }));
|
|
114
|
+
};
|
|
115
|
+
const resetForm = () => {
|
|
116
|
+
updateForms();
|
|
117
|
+
};
|
|
118
|
+
const eventBus = useEventBus();
|
|
119
|
+
onMounted(() => {
|
|
120
|
+
eventBus.on("resetFilters", resetForm);
|
|
121
|
+
});
|
|
122
|
+
onUnmounted(() => {
|
|
123
|
+
eventBus.off("resetFilters", resetForm);
|
|
124
|
+
});
|
|
125
|
+
</script>
|
|
126
|
+
<template>
|
|
127
|
+
<form @submit.prevent="() => submitForm()">
|
|
128
|
+
<div :class="css">
|
|
129
|
+
<div v-for="(g, i) in data" :key="`index_${i}`">
|
|
130
|
+
<template v-for="f in g" :key="f.uid">
|
|
131
|
+
<template v-if="!f.isHidden">
|
|
132
|
+
<DefaultInput
|
|
133
|
+
:type="f.type"
|
|
134
|
+
:label="f.label"
|
|
135
|
+
:id="f.uid"
|
|
136
|
+
v-if="['text', 'select', 'date', 'email'].includes(f.type)"
|
|
137
|
+
:options="f.options ? f.options : [[]]"
|
|
138
|
+
v-model="state.formData[f.uid]"
|
|
139
|
+
:errorVuelidate="v$.formData[f.uid].$errors"
|
|
140
|
+
class="mb-2"
|
|
141
|
+
@change="
|
|
142
|
+
(ev: any) => {
|
|
143
|
+
if (f.onChangeValue) {
|
|
144
|
+
f.onChangeValue(state.formData, ev);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
"
|
|
148
|
+
/>
|
|
149
|
+
<DefaultDateSelection
|
|
150
|
+
:id="f.uid"
|
|
151
|
+
:label="f.label"
|
|
152
|
+
v-if="f.type === 'range'"
|
|
153
|
+
mode="interval"
|
|
154
|
+
v-model="state.formData[f.uid]"
|
|
155
|
+
class="mb-2"
|
|
156
|
+
/>
|
|
157
|
+
</template>
|
|
158
|
+
</template>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="flex justify-between mt-2 gap-x-2">
|
|
163
|
+
<button type="submit" class="btn defaults primary">
|
|
164
|
+
{{ $t("filters_search_cta") }}
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
type="reset"
|
|
168
|
+
class="btn defaults neutral"
|
|
169
|
+
@click.prevent="
|
|
170
|
+
() => {
|
|
171
|
+
resetForm();
|
|
172
|
+
}
|
|
173
|
+
"
|
|
174
|
+
>
|
|
175
|
+
{{ $t("filters_clear_cta") }}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
</form>
|
|
179
|
+
</template>
|