mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-20 22:53:49 +00:00
Allow users to display news on homepage (+ configuring a dashboard homepage)
This commit is contained in:
@@ -97,6 +97,8 @@ import MyNotifications from './MyNotifications';
|
||||
import MyWorkflows from './MyWorkflows.vue';
|
||||
import TabCounter from './TabCounter';
|
||||
import { mapState } from "vuex";
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
@@ -112,7 +114,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'MyCustoms'
|
||||
activeTab: 'MyCustoms',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -126,8 +128,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
selectTab(tab) {
|
||||
this.$store.dispatch('getByTab', { tab: tab });
|
||||
if (tab !== 'MyCustoms') {
|
||||
this.$store.dispatch('getByTab', { tab: tab });
|
||||
}
|
||||
this.activeTab = tab;
|
||||
console.log(this.activeTab)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ $t('widget.news.title') }}</h1>
|
||||
<ul v-if="newsItems.length > 0" class="scrollable">
|
||||
<NewsItem v-for="item in newsItems" :item="item" :key="item.id" />
|
||||
</ul>
|
||||
<p v-if="newsItems.length === 0 " class="chill-no-data-statement">{{ $t('widget.news.none') }}</p>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { fetchResults } from '../../../lib/api/apiMethods';
|
||||
import Modal from '../../_components/Modal.vue';
|
||||
import { NewsItemType } from '../../../types';
|
||||
import NewsItem from './NewsItem.vue';
|
||||
|
||||
const newsItems = ref<NewsItemType[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
fetchResults<NewsItemType>('/api/1.0/main/news/current.json')
|
||||
.then((news): Promise<void> => {
|
||||
// console.log('news articles', response.results)
|
||||
newsItems.value = news;
|
||||
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch((error: string) => {
|
||||
console.error('Error fetching news items', error);
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<li>
|
||||
<h2>{{ props.item.title }}</h2>
|
||||
<time class="createdBy" datetime="{{item.startDate.datetime}}">{{ $d(newsItemStartDate(), 'text') }}</time>
|
||||
<div class="content" v-if="shouldTruncate(item.content)">
|
||||
<div v-html="prepareContent(item.content)"></div>
|
||||
<div class="float-end">
|
||||
<button class="btn btn-sm btn-show read-more" @click="() => openModal(item)">{{ $t('widget.news.readMore') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-else>
|
||||
<div v-html="convertMarkdownToHtml(item.content)"></div>
|
||||
</div>
|
||||
|
||||
<modal v-if="showModal" @close="closeModal">
|
||||
<template #header>
|
||||
<p class="news-title">{{ item.title }}</p>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="news-date">
|
||||
<time class="createdBy" datetime="{{item.startDate.datetime}}">{{ $d(newsItemStartDate(), 'text') }}</time>
|
||||
</p>
|
||||
<div v-html="convertMarkdownToHtml(item.content)"></div>
|
||||
</template>
|
||||
</modal>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { DateTime, NewsItemType } from "../../../types";
|
||||
import type { PropType } from 'vue'
|
||||
import { ref } from "vue";
|
||||
import {ISOToDatetime} from '../../../chill/js/date';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as PropType<NewsItemType>,
|
||||
required: true
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 350,
|
||||
},
|
||||
maxLines: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 3
|
||||
}
|
||||
})
|
||||
|
||||
const selectedArticle = ref<NewsItemType | null>(null);
|
||||
const showModal = ref(false);
|
||||
|
||||
const openModal = (item: NewsItemType) => {
|
||||
selectedArticle.value = item;
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
selectedArticle.value = null;
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
const shouldTruncate = (content: string): boolean => {
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Check if any line exceeds the maximum length
|
||||
const tooManyLines = lines.length > props.maxLines;
|
||||
|
||||
return content.length > props.maxLength || tooManyLines;
|
||||
};
|
||||
|
||||
const truncateContent = (content: string): string => {
|
||||
let truncatedContent = content.slice(0, props.maxLength);
|
||||
let linkDepth = 0;
|
||||
let linkStartIndex = -1;
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Truncate if amount of lines are too many
|
||||
if (lines.length > props.maxLines && content.length < props.maxLength) {
|
||||
const truncatedContent = lines.slice(0, props.maxLines).join('\n').trim();
|
||||
return truncatedContent + '...';
|
||||
}
|
||||
|
||||
for (let i = 0; i < truncatedContent.length; i++) {
|
||||
const char = truncatedContent[i];
|
||||
|
||||
if (char === '[') {
|
||||
linkDepth++;
|
||||
if (linkDepth === 1) {
|
||||
linkStartIndex = i;
|
||||
}
|
||||
} else if (char === ']') {
|
||||
linkDepth = Math.max(0, linkDepth - 1);
|
||||
} else if (char === '(' && linkDepth === 0) {
|
||||
truncatedContent = truncatedContent.slice(0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (linkDepth > 0) {
|
||||
truncatedContent += ']';
|
||||
linkDepth--;
|
||||
}
|
||||
|
||||
// If a link was found, append the URL inside the parentheses
|
||||
if (linkStartIndex !== -1) {
|
||||
const linkEndIndex = content.indexOf(')', linkStartIndex);
|
||||
const url = content.slice(linkStartIndex + 1, linkEndIndex);
|
||||
truncatedContent = truncatedContent.slice(0, linkStartIndex) + `(${url})`;
|
||||
}
|
||||
|
||||
truncatedContent += '...';
|
||||
|
||||
return truncatedContent;
|
||||
};
|
||||
|
||||
const preprocess = (markdown: string): string => {
|
||||
return markdown;
|
||||
}
|
||||
|
||||
const postprocess = (html: string): string => {
|
||||
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
||||
if ('target' in node) {
|
||||
node.setAttribute('target', '_blank');
|
||||
node.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) {
|
||||
node.setAttribute('xlink:show', 'new');
|
||||
}
|
||||
})
|
||||
|
||||
return DOMPurify.sanitize(html);
|
||||
}
|
||||
|
||||
const convertMarkdownToHtml = (markdown: string): string => {
|
||||
marked.use({'hooks': {postprocess, preprocess}});
|
||||
const rawHtml = marked(markdown);
|
||||
return rawHtml;
|
||||
};
|
||||
|
||||
const prepareContent = (content: string): string => {
|
||||
const htmlContent = convertMarkdownToHtml(content);
|
||||
return truncateContent(htmlContent);
|
||||
};
|
||||
|
||||
const newsItemStartDate = (): null|Date => {
|
||||
return ISOToDatetime(props.item?.startDate.datetime);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
li {
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
padding: .8rem;
|
||||
background-color: #fbfbfb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1rem !important;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
font-size: .9rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
@@ -1,76 +1,73 @@
|
||||
<template>
|
||||
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_dashboard') }}</span>
|
||||
<div v-else id="dashboards" class="row g-3" data-masonry='{"percentPosition": true }'>
|
||||
|
||||
<div class="mbloc col col-sm-6 col-lg-4">
|
||||
<div class="custom1">
|
||||
<ul class="list-unstyled">
|
||||
<li v-if="counter.notifications > 0">
|
||||
<i18n-t keypath="counter.unread_notifications" tag="span" :class="counterClass" :plural="counter.notifications">
|
||||
<template v-slot:n><span>{{ counter.notifications }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.accompanyingCourses > 0">
|
||||
<i18n-t keypath="counter.assignated_courses" tag="span" :class="counterClass" :plural="counter.accompanyingCourses">
|
||||
<template v-slot:n><span>{{ counter.accompanyingCourses }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.works > 0">
|
||||
<i18n-t keypath="counter.assignated_actions" tag="span" :class="counterClass" :plural="counter.works">
|
||||
<template v-slot:n><span>{{ counter.works }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.evaluations > 0">
|
||||
<i18n-t keypath="counter.assignated_evaluations" tag="span" :class="counterClass" :plural="counter.evaluations">
|
||||
<template v-slot:n><span>{{ counter.evaluations }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.tasksAlert > 0">
|
||||
<i18n-t keypath="counter.alert_tasks" tag="span" :class="counterClass" :plural="counter.tasksAlert">
|
||||
<template v-slot:n><span>{{ counter.tasksAlert }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.tasksWarning > 0">
|
||||
<i18n-t keypath="counter.warning_tasks" tag="span" :class="counterClass" :plural="counter.tasksWarning">
|
||||
<template v-slot:n><span>{{ counter.tasksWarning }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="mbloc col col-sm-6 col-lg-4">
|
||||
<div class="custom2">
|
||||
Mon dashboard personnalisé
|
||||
</div>
|
||||
</div>
|
||||
<div class="mbloc col col-sm-6 col-lg-4">
|
||||
<div class="custom3">
|
||||
Mon dashboard personnalisé
|
||||
</div>
|
||||
</div>
|
||||
<div class="mbloc col col-sm-6 col-lg-4">
|
||||
<div class="custom4">
|
||||
Mon dashboard personnalisé
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div v-else id="dashboards" class="container g-3">
|
||||
<div class="row">
|
||||
<div class="mbloc col-xs-12 col-sm-4">
|
||||
<div class="custom1">
|
||||
<ul class="list-unstyled">
|
||||
<li v-if="counter.notifications > 0">
|
||||
<i18n-t keypath="counter.unread_notifications" tag="span" :class="counterClass" :plural="counter.notifications">
|
||||
<template v-slot:n><span>{{ counter.notifications }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.accompanyingCourses > 0">
|
||||
<i18n-t keypath="counter.assignated_courses" tag="span" :class="counterClass" :plural="counter.accompanyingCourses">
|
||||
<template v-slot:n><span>{{ counter.accompanyingCourses }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.works > 0">
|
||||
<i18n-t keypath="counter.assignated_actions" tag="span" :class="counterClass" :plural="counter.works">
|
||||
<template v-slot:n><span>{{ counter.works }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.evaluations > 0">
|
||||
<i18n-t keypath="counter.assignated_evaluations" tag="span" :class="counterClass" :plural="counter.evaluations">
|
||||
<template v-slot:n><span>{{ counter.evaluations }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.tasksAlert > 0">
|
||||
<i18n-t keypath="counter.alert_tasks" tag="span" :class="counterClass" :plural="counter.tasksAlert">
|
||||
<template v-slot:n><span>{{ counter.tasksAlert }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li v-if="counter.tasksWarning > 0">
|
||||
<i18n-t keypath="counter.warning_tasks" tag="span" :class="counterClass" :plural="counter.tasksWarning">
|
||||
<template v-slot:n><span>{{ counter.tasksWarning }}</span></template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="this.hasDashboardItems">
|
||||
<template v-for="dashboardItem in this.dashboardItems">
|
||||
<div class="mbloc col-xs-12 col-sm-8 news" v-if="dashboardItem.type === 'news'">
|
||||
<News />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import Masonry from 'masonry-layout/masonry';
|
||||
import {makeFetch} from "ChillMainAssets/lib/api/apiMethods";
|
||||
import News from './DashboardWidgets/News.vue';
|
||||
|
||||
export default {
|
||||
name: "MyCustoms",
|
||||
components: {
|
||||
News
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
counterClass: {
|
||||
counter: true //hack to pass class 'counter' in i18n-t
|
||||
}
|
||||
},
|
||||
dashboardItems: [],
|
||||
masonry: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -78,11 +75,19 @@ export default {
|
||||
noResults() {
|
||||
return false
|
||||
},
|
||||
hasDashboardItems() {
|
||||
return this.dashboardItems.length > 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const elem = document.querySelector('#dashboards');
|
||||
const masonry = new Masonry(elem, {});
|
||||
}
|
||||
makeFetch('GET', '/api/1.0/main/dashboard-config-item.json')
|
||||
.then((response) => {
|
||||
this.dashboardItems = response;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,4 +103,10 @@ span.counter {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
div.news {
|
||||
max-height: 22rem;
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
|
@@ -63,7 +63,15 @@ const appMessages = {
|
||||
},
|
||||
emergency: "Urgent",
|
||||
confidential: "Confidentiel",
|
||||
automatic_notification: "Notification automatique"
|
||||
automatic_notification: "Notification automatique",
|
||||
widget: {
|
||||
news: {
|
||||
title: "Actualités",
|
||||
readMore: "Lire la suite",
|
||||
date: "Date",
|
||||
none: "Aucune actualité"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -96,13 +96,11 @@ const store = createStore({
|
||||
},
|
||||
catchError(state, error) {
|
||||
state.errorMsg.push(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
getByTab({ commit, getters }, { tab, param }) {
|
||||
switch (tab) {
|
||||
case 'MyCustoms':
|
||||
break;
|
||||
// case 'MyWorks':
|
||||
// if (!getters.isWorksLoaded) {
|
||||
// commit('setLoading', true);
|
||||
@@ -221,8 +219,8 @@ const store = createStore({
|
||||
default:
|
||||
throw 'tab '+ tab;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { store };
|
||||
export { store };
|
||||
|
@@ -15,18 +15,20 @@ import AddressModal from "./AddressModal.vue";
|
||||
|
||||
export interface AddressModalContentProps {
|
||||
address_id: number;
|
||||
address_ref_status: AddressRefStatus | null;
|
||||
address_ref_status: AddressRefStatus;
|
||||
}
|
||||
|
||||
const data = reactive<{
|
||||
loading: boolean,
|
||||
working_address: Address | null,
|
||||
working_ref_status: AddressRefStatus | null,
|
||||
}>({
|
||||
interface AddressModalData {
|
||||
loading: boolean,
|
||||
working_address: Address | null,
|
||||
working_ref_status: AddressRefStatus | null,
|
||||
}
|
||||
|
||||
const data: AddressModalData = reactive({
|
||||
loading: false,
|
||||
working_address: null,
|
||||
working_ref_status: null,
|
||||
});
|
||||
} as AddressModalData);
|
||||
|
||||
const props = defineProps<AddressModalContentProps>();
|
||||
|
||||
|
@@ -51,7 +51,7 @@ const messages = {
|
||||
years_old: "1 an | {n} an | {n} ans",
|
||||
residential_address: "Adresse de résidence",
|
||||
located_at: "réside chez"
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user