added unread and read all function with endpoints for notifications

This commit is contained in:
Ronchie Blondiau 2024-07-05 13:36:31 +00:00 committed by Julien Fastré
parent 2b09e1459c
commit 2d67843901
14 changed files with 805 additions and 377 deletions

View File

@ -0,0 +1,7 @@
kind: Feature
body: |+
Add the possibility to mark all notifications as read
time: 2024-07-05T15:21:11.730543489+02:00
custom:
Issue: "273"

View File

@ -104,4 +104,38 @@ class NotificationApiController
return new JsonResponse(null, JsonResponse::HTTP_ACCEPTED, [], false); return new JsonResponse(null, JsonResponse::HTTP_ACCEPTED, [], false);
} }
/**
* @Route("/mark/allread", name="chill_api_main_notification_mark_allread", methods={"POST"})
*/
public function markAllRead(): JsonResponse
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new \RuntimeException('Invalid user');
}
$modifiedNotificationIds = $this->notificationRepository->markAllNotificationAsReadForUser($user);
return new JsonResponse($modifiedNotificationIds);
}
/**
* @Route("/mark/undoallread", name="chill_api_main_notification_mark_undoallread", methods={"POST"})
*/
public function undoAllRead(Request $request): JsonResponse
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new \RuntimeException('Invalid user');
}
$ids = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
$touchedIds = $this->notificationRepository->markAllNotificationAsUnreadForUser($user, $ids);
return new JsonResponse($touchedIds);
}
} }

View File

@ -193,13 +193,17 @@ class NotificationController extends AbstractController
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$currentUser = $this->security->getUser(); $currentUser = $this->security->getUser();
if (!$currentUser instanceof User) {
throw new AccessDeniedHttpException('Only regular user should access this page');
}
$notificationsNbr = $this->notificationRepository->countAllForAttendee($currentUser); $notificationsNbr = $this->notificationRepository->countAllForAttendee($currentUser);
$paginator = $this->paginatorFactory->create($notificationsNbr); $paginator = $this->paginatorFactory->create($notificationsNbr);
$notifications = $this->notificationRepository->findAllForAttendee( $notifications = $this->notificationRepository->findAllForAttendee(
$currentUser, $currentUser,
$limit = $paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$offset = $paginator->getCurrentPage()->getFirstItemNumber() $paginator->getCurrentPage()->getFirstItemNumber()
); );
return $this->render('@ChillMain/Notification/list.html.twig', [ return $this->render('@ChillMain/Notification/list.html.twig', [

View File

@ -13,6 +13,8 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Statement; use Doctrine\DBAL\Statement;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -81,10 +83,7 @@ final class NotificationRepository implements ObjectRepository
$results->free(); $results->free();
} else { } else {
$wheres = []; $wheres = [];
foreach ([ foreach ([['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId], ...$more] as $k => ['relatedEntityClass' => $relClass, 'relatedEntityId' => $relId]) {
['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId],
...$more,
] as $k => ['relatedEntityClass' => $relClass, 'relatedEntityId' => $relId]) {
$wheres[] = "(relatedEntityClass = :relatedEntityClass_{$k} AND relatedEntityId = :relatedEntityId_{$k})"; $wheres[] = "(relatedEntityClass = :relatedEntityClass_{$k} AND relatedEntityId = :relatedEntityId_{$k})";
$sqlParams["relatedEntityClass_{$k}"] = $relClass; $sqlParams["relatedEntityClass_{$k}"] = $relClass;
$sqlParams["relatedEntityId_{$k}"] = $relId; $sqlParams["relatedEntityId_{$k}"] = $relId;
@ -228,11 +227,11 @@ final class NotificationRepository implements ObjectRepository
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn'); $rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '. $sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
'FROM chill_main_notification cmn '. 'FROM chill_main_notification cmn '.
'WHERE '. 'WHERE '.
'EXISTS (select 1 FROM chill_main_notification_addresses_unread cmnau WHERE cmnau.user_id = :userId and cmnau.notification_id = cmn.id) '. 'EXISTS (select 1 FROM chill_main_notification_addresses_unread cmnau WHERE cmnau.user_id = :userId and cmnau.notification_id = cmn.id) '.
'ORDER BY cmn.date DESC '. 'ORDER BY cmn.date DESC '.
'LIMIT :limit OFFSET :offset'; 'LIMIT :limit OFFSET :offset';
$nq = $this->em->createNativeQuery($sql, $rsm) $nq = $this->em->createNativeQuery($sql, $rsm)
->setParameter('userId', $user->getId()) ->setParameter('userId', $user->getId())
@ -255,10 +254,12 @@ final class NotificationRepository implements ObjectRepository
$qb = $this->repository->createQueryBuilder('n'); $qb = $this->repository->createQueryBuilder('n');
// add condition for related entity (in main arguments, and in more) // add condition for related entity (in main arguments, and in more)
$or = $qb->expr()->orX($qb->expr()->andX( $or = $qb->expr()->orX(
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'), $qb->expr()->andX(
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId') $qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'),
)); $qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')
)
);
$qb $qb
->setParameter('relatedEntityClass', $relatedEntityClass) ->setParameter('relatedEntityClass', $relatedEntityClass)
->setParameter('relatedEntityId', $relatedEntityId); ->setParameter('relatedEntityId', $relatedEntityId);
@ -310,4 +311,86 @@ final class NotificationRepository implements ObjectRepository
return $qb; return $qb;
} }
/**
* @return list<int> the ids of the notifications marked as unread
*/
public function markAllNotificationAsReadForUser(User $user): array
{
// Get the database connection from the entity manager
$connection = $this->em->getConnection();
/** @var Result $results */
$results = $connection->transactional(function (Connection $connection) use ($user) {
// Define the SQL query
$sql = <<<'SQL'
DELETE FROM chill_main_notification_addresses_unread
WHERE user_id = :user_id
RETURNING notification_id
SQL;
return $connection->executeQuery($sql, ['user_id' => $user->getId()]);
});
$notificationIdsTouched = [];
foreach ($results->iterateAssociative() as $row) {
$notificationIdsTouched[] = $row['notification_id'];
}
return array_values($notificationIdsTouched);
}
/**
* @param list<int> $notificationIds
*/
public function markAllNotificationAsUnreadForUser(User $user, array $notificationIds): array
{
// Get the database connection from the entity manager
$connection = $this->em->getConnection();
/** @var Result $results */
$results = $connection->transactional(function (Connection $connection) use ($user, $notificationIds) {
// This query double-check that the user is one of the addresses of the notification or the sender,
// if the notification is already marked as unread, this query does not fails.
// this query return the list of notification id which are affected
$sql = <<<'SQL'
INSERT INTO chill_main_notification_addresses_unread (user_id, notification_id)
SELECT ?, chill_main_notification_addresses_user.notification_id
FROM chill_main_notification_addresses_user JOIN chill_main_notification ON chill_main_notification_addresses_user.notification_id = chill_main_notification.id
WHERE (chill_main_notification_addresses_user.user_id = ? OR chill_main_notification.sender_id = ?)
AND chill_main_notification_addresses_user.notification_id IN ({ notification_ids })
ON CONFLICT (user_id, notification_id) DO NOTHING
RETURNING notification_id
SQL;
$params = [$user->getId(), $user->getId(), $user->getId(), ...array_values($notificationIds)];
$sql = strtr($sql, ['{ notification_ids }' => implode(', ', array_fill(0, count($notificationIds), '?'))]);
return $connection->executeQuery($sql, $params);
});
$notificationIdsTouched = [];
foreach ($results->iterateAssociative() as $row) {
$notificationIdsTouched[] = $row['notification_id'];
}
return array_values($notificationIdsTouched);
}
public function findAllUnreadByUser(User $user): array
{
$rsm = new Query\ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
'FROM chill_main_notification cmn '.
'WHERE '.
'EXISTS (SELECT 1 FROM chill_main_notification_addresses_unread cmnau WHERE cmnau.user_id = :userId AND cmnau.notification_id = cmn.id) '.
'ORDER BY cmn.date DESC';
$nq = $this->em->createNativeQuery($sql, $rsm)
->setParameter('userId', $user->getId());
return $nq->getResult();
}
} }

View File

@ -1,14 +1,16 @@
import {createApp} from "vue"; import { createApp } from "vue";
import NotificationReadToggle from "ChillMainAssets/vuejs/_components/Notification/NotificationReadToggle.vue"; import NotificationReadToggle from "ChillMainAssets/vuejs/_components/Notification/NotificationReadToggle.vue";
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n"; import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import NotificationReadAllToggle from "ChillMainAssets/vuejs/_components/Notification/NotificationReadAllToggle.vue";
const i18n = _createI18n({}); const i18n = _createI18n({});
window.addEventListener('DOMContentLoaded', function (e) { window.addEventListener("DOMContentLoaded", function (e) {
document.querySelectorAll('.notification_toggle_read_status') document
.forEach(function (el, i) { .querySelectorAll(".notification_toggle_read_status")
createApp({ .forEach(function (el, i) {
template: `<notification-read-toggle createApp({
template: `<notification-read-toggle
:notificationId="notificationId" :notificationId="notificationId"
:buttonClass="buttonClass" :buttonClass="buttonClass"
:buttonNoText="buttonNoText" :buttonNoText="buttonNoText"
@ -17,40 +19,45 @@ window.addEventListener('DOMContentLoaded', function (e) {
@markRead="onMarkRead" @markRead="onMarkRead"
@markUnread="onMarkUnread"> @markUnread="onMarkUnread">
</notification-read-toggle>`, </notification-read-toggle>`,
components: { components: {
NotificationReadToggle, NotificationReadToggle,
}, },
data() { data() {
return { return {
notificationId: el.dataset.notificationId, notificationId: parseInt(el.dataset.notificationId),
buttonClass: el.dataset.buttonClass, buttonClass: el.dataset.buttonClass,
buttonNoText: 'false' === el.dataset.buttonText, buttonNoText: "false" === el.dataset.buttonText,
showUrl: el.dataset.showButtonUrl, showUrl: el.dataset.showButtonUrl,
isRead: 1 === Number.parseInt(el.dataset.notificationCurrentIsRead), isRead: 1 === Number.parseInt(el.dataset.notificationCurrentIsRead),
container: el.dataset.container container: el.dataset.container,
} };
}, },
computed: { computed: {
getContainer() { getContainer() {
return document.querySelectorAll(`div.${this.container}`); return document.querySelectorAll(`div.${this.container}`);
} },
}, },
methods: { methods: {
onMarkRead() { onMarkRead() {
if (typeof this.getContainer[i] !== 'undefined') { if (typeof this.getContainer[i] !== "undefined") {
this.getContainer[i].classList.replace('read', 'unread'); this.getContainer[i].classList.replace("read", "unread");
} else { throw 'data-container attribute is missing' } } else {
this.isRead = false; throw "data-container attribute is missing";
}, }
onMarkUnread() { this.isRead = false;
if (typeof this.getContainer[i] !== 'undefined') { },
this.getContainer[i].classList.replace('unread', 'read'); onMarkUnread() {
} else { throw 'data-container attribute is missing' } if (typeof this.getContainer[i] !== "undefined") {
this.isRead = true; this.getContainer[i].classList.replace("unread", "read");
}, } else {
} throw "data-container attribute is missing";
}) }
.use(i18n) this.isRead = true;
.mount(el); },
}); },
})
.use(i18n)
.mount(el);
});
}); });

View File

@ -0,0 +1,39 @@
import { createApp } from "vue";
import { _createI18n } from "../../vuejs/_js/i18n";
import NotificationReadAllToggle from "../../vuejs/_components/Notification/NotificationReadAllToggle.vue";
const i18n = _createI18n({});
document.addEventListener("DOMContentLoaded", function () {
const elements = document.querySelectorAll(".notification_all_read");
elements.forEach((element) => {
console.log('launch');
createApp({
template: `<notification-read-all-toggle @markAsRead="markAsRead" @markAsUnRead="markAsUnread"></notification-read-all-toggle>`,
components: {
NotificationReadAllToggle,
},
methods: {
markAsRead(id: number) {
const el = document.querySelector<HTMLDivElement>(`div.notification-status[data-notification-id="${id}"]`);
if (el === null) {
return;
}
el.classList.add('read');
el.classList.remove('unread');
},
markAsUnread(id: number) {
const el = document.querySelector<HTMLDivElement>(`div.notification-status[data-notification-id="${id}"]`);
if (el === null) {
return;
}
el.classList.remove('read');
el.classList.add('unread');
},
}
})
.use(i18n)
.mount(element);
});
});

View File

@ -0,0 +1,50 @@
<template>
<div>
<button v-if="idsMarkedAsRead.length === 0"
class="btn btn-primary"
type="button"
@click="markAllRead"
>
<i class="fa fa-sm fa-envelope-open-o"></i> Marquer tout comme lu
</button>
<button v-else
class="btn btn-primary"
type="button"
@click="undo"
>
<i class="fa fa-sm fa-envelope-open-o"></i> Annuler
</button>
</div>
</template>
<script lang="ts" setup>
import { makeFetch } from "../../../lib/api/apiMethods";
import { ref } from "vue";
const emit = defineEmits<{
(e: 'markAsRead', id: number): void,
(e: 'markAsUnRead', id: number): void,
}>();
const idsMarkedAsRead = ref([] as number[]);
async function markAllRead() {
const ids: number[] = await makeFetch("POST", `/api/1.0/main/notification/mark/allread`, null);
for (let i of ids) {
idsMarkedAsRead.value.push(i);
emit('markAsRead', i);
}
}
async function undo() {
const touched: number[] = await makeFetch("POST", `/api/1.0/main/notification/mark/undoallread`, idsMarkedAsRead.value);
while (idsMarkedAsRead.value.length > 0) {
idsMarkedAsRead.value.pop();
}
for (let t of touched) {
emit('markAsUnRead', t);
}
};
</script>
<style lang="scss" scoped></style>

View File

@ -1,47 +1,66 @@
<template> <template>
<div :class="{'btn-group btn-group-sm float-end': isButtonGroup }" <div
role="group" aria-label="Notification actions"> :class="{ 'btn-group btn-group-sm float-end': isButtonGroup }"
role="group"
<button v-if="isRead" aria-label="Notification actions"
class="btn" >
:class="overrideClass" <button
type="button" v-if="isRead"
:title="$t('markAsUnread')" class="btn"
@click="markAsUnread" :class="overrideClass"
> type="button"
:title="$t('markAsUnread')"
@click="markAsUnread"
>
<i class="fa fa-sm fa-envelope-o"></i> <i class="fa fa-sm fa-envelope-o"></i>
<span v-if="!buttonNoText" class="ps-2"> <span v-if="!buttonNoText" class="ps-2">
{{ $t('markAsUnread') }} {{ $t("markAsUnread") }}
</span> </span>
</button> </button>
<button v-if="!isRead" <button
class="btn" v-if="!isRead"
:class="overrideClass" class="btn"
type="button" :class="overrideClass"
:title="$t('markAsRead')" type="button"
@click="markAsRead" :title="$t('markAsRead')"
> @click="markAsRead"
>
<i class="fa fa-sm fa-envelope-open-o"></i> <i class="fa fa-sm fa-envelope-open-o"></i>
<span v-if="!buttonNoText" class="ps-2"> <span v-if="!buttonNoText" class="ps-2">
{{ $t('markAsRead') }} {{ $t("markAsRead") }}
</span> </span>
</button> </button>
<a v-if="isButtonGroup" <a
v-if="isButtonGroup"
type="button" type="button"
class="btn btn-outline-primary" class="btn btn-outline-primary"
:href="showUrl" :href="showUrl"
:title="$t('action.show')" :title="$t('action.show')"
> >
<i class="fa fa-sm fa-comment-o"></i> <i class="fa fa-sm fa-comment-o"></i>
</a> </a>
<!-- "Mark All Read" button -->
<button
v-if="showMarkAllButton"
class="btn"
:class="overrideClass"
type="button"
:title="$t('markAllRead')"
@click="markAllRead"
>
<i class="fa fa-sm fa-envelope-o"></i>
<span v-if="!buttonNoText" class="ps-2">
{{ $t("markAllRead") }}
</span>
</button>
</div> </div>
</template> </template>
<script> <script>
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.ts'; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts";
export default { export default {
name: "NotificationReadToggle", name: "NotificationReadToggle",
@ -57,7 +76,7 @@ export default {
// Optional // Optional
buttonClass: { buttonClass: {
required: false, required: false,
type: String type: String,
}, },
buttonNoText: { buttonNoText: {
required: false, required: false,
@ -65,14 +84,14 @@ export default {
}, },
showUrl: { showUrl: {
required: false, required: false,
type: String type: String,
} },
}, },
emits: ['markRead', 'markUnread'], emits: ["markRead", "markUnread"],
computed: { computed: {
/// [Option] override default button appearance (btn-misc) /// [Option] override default button appearance (btn-misc)
overrideClass() { overrideClass() {
return this.buttonClass ? this.buttonClass : 'btn-misc' return this.buttonClass ? this.buttonClass : "btn-misc";
}, },
/// [Option] don't display text on button /// [Option] don't display text on button
buttonHideText() { buttonHideText() {
@ -82,31 +101,48 @@ export default {
// When passed, the component return a button-group with 2 buttons. // When passed, the component return a button-group with 2 buttons.
isButtonGroup() { isButtonGroup() {
return this.showUrl; return this.showUrl;
} },
}, },
methods: { methods: {
markAsUnread() { markAsUnread() {
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/unread`, []).then(response => { makeFetch(
this.$emit('markRead', { notificationId: this.notificationId }); "POST",
}) `/api/1.0/main/notification/${this.notificationId}/mark/unread`,
[]
).then((response) => {
this.$emit("markRead", {notificationId: this.notificationId});
});
}, },
markAsRead() { markAsRead() {
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/read`, []).then(response => { makeFetch(
this.$emit('markUnread', { notificationId: this.notificationId }); "POST",
}) `/api/1.0/main/notification/${this.notificationId}/mark/read`,
[]
).then((response) => {
this.$emit("markUnread", {
notificationId: this.notificationId,
});
});
},
markAllRead() {
makeFetch(
"POST",
`/api/1.0/main/notification/markallread`,
[]
).then((response) => {
this.$emit("markAllRead");
});
}, },
}, },
i18n: { i18n: {
messages: { messages: {
fr: { fr: {
markAsUnread: 'Marquer comme non-lu', markAsUnread: "Marquer comme non-lu",
markAsRead: 'Marquer comme lu' markAsRead: "Marquer comme lu",
} },
} },
} },
} };
</script> </script>
<style lang="scss"> <style lang="scss"></style>
</style>

View File

@ -1,30 +1,33 @@
{% macro title(c) %} {% macro title(c) %}
<div class="item-row title"> <div class="item-row title">
<h2 class="notification-title"> <h2 class="notification-title">
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}"> <a
href="{{ chill_path_add_return_path('chill_main_notification_show', {
id: c.notification.id
}) }}"
>
{{ c.notification.title }} {{ c.notification.title }}
</a> </a>
</h2> </h2>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro header(c) %} {% macro header(c) %}
<div class="item-row notification-header mt-2"> <div class="item-row notification-header mt-2">
<div class="item-col"> <div class="item-col">
<ul class="small_in_title"> <ul class="small_in_title">
{% if c.step is not defined or c.step == 'inbox' %} {% if c.step is not defined or c.step == 'inbox' %}
<li class="notification-from"> <li class="notification-from">
<span class="item-key"> <span class="item-key">
<abbr title="{{ 'notification.received_from'|trans }}"> <abbr title="{{ 'notification.received_from' | trans }}">
{{ 'notification.from'|trans }} : {{ "notification.from" | trans }} :
</abbr> </abbr>
</span> </span>
{% if not c.notification.isSystem %} {% if not c.notification.isSystem %}
<span class="badge-user"> <span class="badge-user">
{{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }} {{ c.notification.sender | chill_entity_render_string({'at_date': c.notification.date}) }}
</span> </span>
{% else %} {% else %}
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span> <span class="badge-user system">{{ "notification.is_system" | trans }}</span>
{% endif %} {% endif %}
</li> </li>
{% endif %} {% endif %}
@ -32,34 +35,37 @@
<li class="notification-to"> <li class="notification-to">
{% if c.notification_cc is defined %} {% if c.notification_cc is defined %}
{% if c.notification_cc %} {% if c.notification_cc %}
<span class="item-key"> <span class="item-key">
<abbr title="{{ 'notification.sent_cc'|trans }}"> <abbr title="{{ 'notification.sent_cc' | trans }}">
{{ 'notification.cc'|trans }} : {{ "notification.cc" | trans }} :
</abbr> </abbr>
</span> </span>
{% else %} {% else %}
<span class="item-key"> <span class="item-key">
<abbr title="{{ 'notification.sent_to'|trans }}"> <abbr title="{{ 'notification.sent_to' | trans }}">
{{ 'notification.to'|trans }} : {{ "notification.to" | trans }} :
</abbr> </abbr>
</span> </span>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="item-key"> <span class="item-key">
<abbr title="{{ 'notification.sent_to'|trans }}"> <abbr title="{{ 'notification.sent_to' | trans }}">
{{ 'notification.to'|trans }} : {{ "notification.to" | trans }} :
</abbr> </abbr>
</span> </span>
{% endif %} {% endif %}
{% for a in c.notification.addressees %} {% for a in c.notification.addressees %}
<span class="badge-user"> <span class="badge-user">
{{ a|chill_entity_render_string({'at_date': c.notification.date}) }} {{ a | chill_entity_render_string({'at_date': c.notification.date}) }}
</span> </span>
{% endfor %} {% endfor %}
{% for a in c.notification.addressesEmails %} {% for a in c.notification.addressesEmails %}
<span class="badge-user" title="{{ 'notification.Email with access link'|trans|e('html_attr') }}"> <span
{{ a }} class="badge-user"
</span> title="{{ 'notification.Email with access link'|trans|e('html_attr') }}"
>
{{ a }}
</span>
{% endfor %} {% endfor %}
</li> </li>
{% endif %} {% endif %}
@ -70,7 +76,6 @@
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro content(c) %} {% macro content(c) %}
<div class="item-row separator"> <div class="item-row separator">
{% if c.data is defined %} {% if c.data is defined %}
@ -83,60 +88,77 @@
<div class="notification-content"> <div class="notification-content">
{% if c.full_content is defined and c.full_content == true %} {% if c.full_content is defined and c.full_content == true %}
{% if c.notification.message is not empty %} {% if c.notification.message is not empty %}
{{ c.notification.message|chill_markdown_to_html }} {{ c.notification.message | chill_markdown_to_html }}
{% else %} {% else %}
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p> <p class="chill-no-data-statement">{{ "Any comment" | trans }}</p>
{% endif %} {% endif %}
{% else %} {% else %}
{% if c.notification.message is not empty %} {% if c.notification.message is not empty %}
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }} {{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p> <p class="read-more">
<a
href="{{ chill_path_add_return_path('chill_main_notification_show', {
id: c.notification.id
}) }}"
>{{ "Read more" | trans }}</a>
</p>
{% else %} {% else %}
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p> <p class="chill-no-data-statement">{{ "Any comment" | trans }}</p>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro actions(c) %} {% macro actions(c) %}
{% if c.action_button is not defined or c.action_button != false %} {% if c.action_button is not defined or c.action_button != false %}
<div class="item-row separator"> <div class="item-row separator">
<div class="item-col item-meta"> <div class="item-col item-meta">
{% if c.notification.comments|length > 0 %} {% if c.notification.comments|length > 0 %}
<div class="comment-counter"> <div class="comment-counter">
<span class="counter"> <span class="counter">
{{ 'notification.counter comments'|trans({'nb': c.notification.comments|length }) }} {{ 'notification.counter comments'|trans({'nb': c.notification.comments|length }) }}
</span> </span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="item-col"> <div class="item-col">
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
{# Vue component #} {# Vue component #}
<span class="notification_toggle_read_status" <span
data-notification-id="{{ c.notification.id }}" class="notification_toggle_read_status"
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}" data-notification-id="{{ c.notification.id }}"
data-container="notification-status" data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
data-container="notification-status"
></span> ></span>
</li> </li>
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %} {% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
<li> <li>
<a href="{{ chill_path_add_return_path('chill_main_notification_edit', {'id': c.notification.id}) }}" <a
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a> href="{{ chill_path_add_return_path(
'chill_main_notification_edit',
{ id: c.notification.id }
) }}"
class="btn btn-edit"
title="{{ 'Edit' | trans }}"
></a>
</li> </li>
{% endif %} {% endif %}
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %} {% if is_granted('CHILL_MAIN_NOTIFICATION_SEE',
c.notification) %}
<li> <li>
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}" <a
class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}"> href="{{ chill_path_add_return_path(
'chill_main_notification_show',
{ id: c.notification.id }
) }}"
class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}"
title="{{ 'notification.see_comments_thread' | trans }}"
>
{% if not c.notification.isSystem() %} {% if not c.notification.isSystem() %}
<i class="fa fa-comment"></i> <i class="fa fa-comment"></i>
{% else %} {% else %}
{{ 'Read more'|trans }} {{ "Read more" | trans }}
{% endif %} {% endif %}
</a> </a>
</li> </li>
@ -147,24 +169,30 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
<div class="item-bloc notification-status {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}"> <div
class="item-bloc notification-status {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}"
data-notification-id="{{ notification.id|escape('html_attr') }}"
>
{% if fold_item is defined and fold_item != false %} {% if fold_item is defined and fold_item != false %}
<div class="accordion-header" id="flush-heading-{{ notification.id }}"> <div class="accordion-header" id="flush-heading-{{ notification.id }}">
<button type="button" class="accordion-button collapsed" <button
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}" type="button"
aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}"> class="accordion-button collapsed"
data-bs-toggle="collapse"
data-bs-target="#flush-collapse-{{ notification.id }}"
aria-expanded="false"
aria-controls="flush-collapse-{{ notification.id }}"
>
{{ _self.title(_context) }} {{ _self.title(_context) }}
</button> </button>
{{ _self.header(_context) }} {{ _self.header(_context) }}
</div> </div>
<div id="flush-collapse-{{ notification.id }}" <div
id="flush-collapse-{{ notification.id }}"
class="accordion-collapse collapse" class="accordion-collapse collapse"
aria-labelledby="flush-heading-{{ notification.id }}" aria-labelledby="flush-heading-{{ notification.id }}"
data-bs-parent="#notification-fold"> data-bs-parent="#notification-fold"
>
{{ _self.content(_context) }} {{ _self.content(_context) }}
</div> </div>
{{ _self.actions(_context) }} {{ _self.actions(_context) }}
@ -174,5 +202,4 @@
{{ _self.content(_context) }} {{ _self.content(_context) }}
{{ _self.actions(_context) }} {{ _self.actions(_context) }}
{% endif %} {% endif %}
</div> </div>

View File

@ -1,62 +1,78 @@
{% extends "@ChillMain/layout.html.twig" %} {% extends "@ChillMain/layout.html.twig" %}
{% block title 'notification.My own notifications'|trans %} {% block title 'notification.My own notifications'|trans %}
{% block js %} {% block js %}
{{ parent() }} {{ parent() }}
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }} {{ encore_entry_script_tags("mod_notification_toggle_read_status") }}
{{ encore_entry_script_tags("mod_notification_toggle_read_all_status") }}
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{{ parent() }} {{ parent() }}
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }} {{ encore_entry_link_tags("mod_notification_toggle_read_status") }}
{{ encore_entry_link_tags("mod_notification_toggle_read_all_status") }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="col-10 notification notification-list"> <div class="col-10 notification notification-list">
<h1>{{ block('title') }}</h1> <h1>{{ block("title") }}</h1>
<ul class="nav nav-pills justify-content-center">
<li class="nav-item">
<a
class="nav-link {% if step == 'inbox' %}active{% endif %}"
href="{{ path('chill_main_notification_my') }}"
>
{{ "notification.Notifications received" | trans }}
{% if unreads['inbox'] > 0 %}
<span class="badge rounded-pill bg-danger">
{{ unreads["inbox"] }}
</span>
{% endif %}
</a>
</li>
<li class="nav-item">
<a
class="nav-link {% if step == 'sent' %}active{% endif %}"
href="{{ path('chill_main_notification_sent') }}"
>
{{ "notification.Notifications sent" | trans }}
{% if unreads['sent'] > 0 %}
<span class="badge rounded-pill bg-danger">
{{ unreads["sent"] }}
</span>
{% endif %}
</a>
</li>
</ul>
<ul class="nav nav-pills justify-content-center"> {% if datas|length == 0 %} {% if step == 'inbox' %}
<li class="nav-item"> <p class="chill-no-data-statement">
<a class="nav-link {% if step == 'inbox' %}active{% endif %}" href="{{ path('chill_main_notification_my') }}"> {{ "notification.Any notification received" | trans }}
{{ 'notification.Notifications received'|trans }} </p>
{% if unreads['inbox'] > 0 %}
<span class="badge rounded-pill bg-danger">
{{ unreads['inbox'] }}
</span>
{% endif %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if step == 'sent' %}active{% endif %}" href="{{ path('chill_main_notification_sent') }}">
{{ 'notification.Notifications sent'|trans }}
{% if unreads['sent'] > 0 %}
<span class="badge rounded-pill bg-danger">
{{ unreads['sent'] }}
</span>
{% endif %}
</a>
</li>
</ul>
{% if datas|length == 0 %}
{% if step == 'inbox' %}
<p class="chill-no-data-statement">{{ 'notification.Any notification received'|trans }}</p>
{% else %} {% else %}
<p class="chill-no-data-statement">{{ 'notification.Any notification sent'|trans }}</p> <p class="chill-no-data-statement">
{{ "notification.Any notification sent" | trans }}
</p>
{% endif %} {% endif %}
{% else %} {% else %}
<div class="flex-table accordion accordion-flush" id="notification-fold"> <div class="flex-table accordion accordion-flush" id="notification-fold">
{% for data in datas %} {% for data in datas %}
{% set notification = data.notification %} {% set notification = data.notification %}
{% include '@ChillMain/Notification/_list_item.html.twig' with { {% include '@ChillMain/Notification/_list_item.html.twig' with {
'fold_item': true, 'fold_item': true, 'notification_cc': data.template_data.notificationCc
'notification_cc': data.template_data.notificationCc is defined ? data.template_data.notificationCc : false is defined ? data.template_data.notificationCc : false } %}
} %} {% endfor %}
{% endfor %} </div>
</div>
{{ chill_pagination(paginator) }}
{% endif %}
<ul class="record_actions sticky-form-buttons justify-content-end">
<li class="ml-auto d-flex align-items-center gap-2">
<span class="notification_all_read"></span>
</li>
</ul>
</div>
{{ chill_pagination(paginator) }}
{% endif %}
</div>
{% endblock content %} {% endblock content %}

View File

@ -9,7 +9,7 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code. * the LICENSE file that was distributed with this source code.
*/ */
namespace Repository; namespace Chill\MainBundle\Tests\Repository;
use Chill\MainBundle\Entity\NewsItem; use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Repository\NewsItemRepository; use Chill\MainBundle\Repository\NewsItemRepository;

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Repository;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\NotificationRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class NotificationRepositoryTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
private NotificationRepository $repository;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$this->repository = new NotificationRepository($this->entityManager);
}
public function testMarkAllNotificationAsReadForUser(): void
{
$user = $this->entityManager->createQuery('SELECT u FROM '.User::class.' u')
->setMaxResults(1)->getSingleResult();
$notification = (new Notification())
->setRelatedEntityClass('\Dummy')
->setRelatedEntityId(0)
;
$notification->addAddressee($user)->markAsUnreadBy($user);
$this->entityManager->persist($notification);
$this->entityManager->flush();
$notification->markAsUnreadBy($user);
$this->entityManager->flush();
$this->entityManager->refresh($notification);
if ($notification->isReadBy($user)) {
throw new \LogicException('Notification should not be marked as read');
}
$notificationsIds = $this->repository->markAllNotificationAsReadForUser($user);
self::assertContains($notification->getId(), $notificationsIds);
$this->entityManager->clear();
$notification = $this->entityManager->find(Notification::class, $notification->getId());
self::assertTrue($notification->isReadBy($user));
}
public function testMarkAllNotificationAsUnreadForUser(): void
{
$user = $this->entityManager->createQuery('SELECT u FROM '.User::class.' u')
->setMaxResults(1)->getSingleResult();
$notification = (new Notification())
->setRelatedEntityClass('\Dummy')
->setRelatedEntityId(0)
;
$notification->addAddressee($user); // we do not mark the notification as unread by the user
$this->entityManager->persist($notification);
$this->entityManager->flush();
$notification->markAsReadBy($user);
$this->entityManager->flush();
$this->entityManager->refresh($notification);
if (!$notification->isReadBy($user)) {
throw new \LogicException('Notification should be marked as read');
}
$notificationsIds = $this->repository->markAllNotificationAsUnreadForUser($user, [$notification->getId()]);
self::assertContains($notification->getId(), $notificationsIds);
}
}

View File

@ -5,8 +5,8 @@ info:
title: "Chill api" title: "Chill api"
description: "Api documentation for chill. Currently, work in progress" description: "Api documentation for chill. Currently, work in progress"
servers: servers:
- url: "/api" - url: "/api"
description: "Your current dev server" description: "Your current dev server"
components: components:
schemas: schemas:
@ -165,7 +165,6 @@ components:
endDate: endDate:
$ref: "#/components/schemas/Date" $ref: "#/components/schemas/Date"
paths: paths:
/1.0/search.json: /1.0/search.json:
get: get:
@ -182,25 +181,25 @@ paths:
The results are ordered by relevance, from the most to the lowest relevant. The results are ordered by relevance, from the most to the lowest relevant.
parameters: parameters:
- name: q - name: q
in: query in: query
required: true required: true
description: the pattern to search description: the pattern to search
schema: schema:
type: string type: string
- name: type[] - name: type[]
in: query in: query
required: true required: true
description: the type entities amongst the search is performed description: the type entities amongst the search is performed
schema: schema:
type: array type: array
items: items:
type: string type: string
enum: enum:
- person - person
- thirdparty - thirdparty
- user - user
- household - household
responses: responses:
200: 200:
description: "OK" description: "OK"
@ -237,7 +236,7 @@ paths:
minItems: 2 minItems: 2
maxItems: 2 maxItems: 2
postcode: postcode:
$ref: '#/components/schemas/PostalCode' $ref: "#/components/schemas/PostalCode"
steps: steps:
type: string type: string
street: street:
@ -261,21 +260,21 @@ paths:
- address - address
summary: Return an address by id summary: Return an address by id
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The address id description: The address id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
200: 200:
description: "ok" description: "ok"
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Address' $ref: "#/components/schemas/Address"
404: 404:
description: "not found" description: "not found"
401: 401:
@ -285,14 +284,14 @@ paths:
- address - address
summary: patch an address summary: patch an address
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The address id description: The address id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
requestBody: requestBody:
required: true required: true
content: content:
@ -321,7 +320,7 @@ paths:
minItems: 2 minItems: 2
maxItems: 2 maxItems: 2
postcode: postcode:
$ref: '#/components/schemas/PostalCode' $ref: "#/components/schemas/PostalCode"
steps: steps:
type: string type: string
street: street:
@ -344,28 +343,27 @@ paths:
400: 400:
description: "transition cannot be applyed" description: "transition cannot be applyed"
/1.0/main/address/{id}/duplicate.json: /1.0/main/address/{id}/duplicate.json:
post: post:
tags: tags:
- address - address
summary: Duplicate an existing address summary: Duplicate an existing address
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The address id that will be duplicated description: The address id that will be duplicated
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
200: 200:
description: "ok" description: "ok"
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Address' $ref: "#/components/schemas/Address"
404: 404:
description: "not found" description: "not found"
401: 401:
@ -377,12 +375,12 @@ paths:
- address - address
summary: Return a list of all reference addresses summary: Return a list of all reference addresses
parameters: parameters:
- in: query - in: query
name: postal_code name: postal_code
required: false required: false
schema: schema:
type: integer type: integer
description: The id of a postal code to filter the reference addresses description: The id of a postal code to filter the reference addresses
responses: responses:
200: 200:
description: "ok" description: "ok"
@ -392,21 +390,21 @@ paths:
- address - address
summary: Return a reference address by id summary: Return a reference address by id
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The reference address id description: The reference address id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
200: 200:
description: "ok" description: "ok"
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/AddressReference' $ref: "#/components/schemas/AddressReference"
404: 404:
description: "not found" description: "not found"
401: 401:
@ -419,27 +417,27 @@ paths:
- search - search
summary: Return a reference address by id summary: Return a reference address by id
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The reference address id description: The reference address id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
- name: q - name: q
in: query in: query
required: true required: true
description: The search pattern description: The search pattern
schema: schema:
type: string type: string
responses: responses:
200: 200:
description: "ok" description: "ok"
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/AddressReference' $ref: "#/components/schemas/AddressReference"
404: 404:
description: "not found" description: "not found"
401: 401:
@ -452,12 +450,12 @@ paths:
- address - address
summary: Return a list of all postal-code summary: Return a list of all postal-code
parameters: parameters:
- in: query - in: query
name: country name: country
required: false required: false
schema: schema:
type: integer type: integer
description: The id of a country to filter the postal code description: The id of a country to filter the postal code
responses: responses:
200: 200:
description: "ok" description: "ok"
@ -477,7 +475,7 @@ paths:
code: code:
type: string type: string
country: country:
$ref: '#/components/schemas/Country' $ref: "#/components/schemas/Country"
responses: responses:
401: 401:
description: "Unauthorized" description: "Unauthorized"
@ -496,21 +494,21 @@ paths:
- address - address
summary: Return a postal code by id summary: Return a postal code by id
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The postal code id description: The postal code id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
200: 200:
description: "ok" description: "ok"
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/PostalCode' $ref: "#/components/schemas/PostalCode"
404: 404:
description: "not found" description: "not found"
401: 401:
@ -523,25 +521,25 @@ paths:
- search - search
summary: Search a postal code summary: Search a postal code
parameters: parameters:
- name: q - name: q
in: query in: query
required: true required: true
description: The search pattern description: The search pattern
schema: schema:
type: string type: string
- name: country - name: country
in: query in: query
required: false required: false
description: The country id description: The country id
schema: schema:
type: integer type: integer
responses: responses:
200: 200:
description: "ok" description: "ok"
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/PostalCode' $ref: "#/components/schemas/PostalCode"
404: 404:
description: "not found" description: "not found"
400: 400:
@ -561,27 +559,26 @@ paths:
- address - address
summary: Return a country by id summary: Return a country by id
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The country id description: The country id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
200: 200:
description: "ok" description: "ok"
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Country' $ref: "#/components/schemas/Country"
404: 404:
description: "not found" description: "not found"
401: 401:
description: "Unauthorized" description: "Unauthorized"
/1.0/main/user.json: /1.0/main/user.json:
get: get:
tags: tags:
@ -612,21 +609,21 @@ paths:
- user - user
summary: Return a user by id summary: Return a user by id
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The user id description: The user id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
200: 200:
description: "ok" description: "ok"
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/User' $ref: "#/components/schemas/User"
404: 404:
description: "not found" description: "not found"
401: 401:
@ -649,14 +646,14 @@ paths:
- scope - scope
summary: return a list of scopes summary: return a list of scopes
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The scope id description: The scope id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
200: 200:
description: "ok" description: "ok"
@ -724,14 +721,14 @@ paths:
- location - location
summary: Return the given location summary: Return the given location
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The location id description: The location id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
200: 200:
description: "ok" description: "ok"
@ -784,21 +781,21 @@ paths:
id: 1 id: 1
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod' class: 'Chill\PersonBundle\Entity\AccompanyingPeriod'
roles: roles:
- 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE' - "CHILL_PERSON_ACCOMPANYING_PERIOD_SEE"
/1.0/main/notification/{id}/mark/read: /1.0/main/notification/{id}/mark/read:
post: post:
tags: tags:
- notification - notification
summary: mark a notification as read summary: mark a notification as read
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The notification id description: The notification id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
202: 202:
description: "accepted" description: "accepted"
@ -810,23 +807,55 @@ paths:
- notification - notification
summary: mark a notification as unread summary: mark a notification as unread
parameters: parameters:
- name: id - name: id
in: path in: path
required: true required: true
description: The notification id description: The notification id
schema: schema:
type: integer type: integer
format: integer format: integer
minimum: 1 minimum: 1
responses: responses:
202: 202:
description: "accepted" description: "accepted"
403: 403:
description: "unauthorized" description: "unauthorized"
/1.0/main/notification/mark/allread:
post:
tags:
- notification
summary: Mark all notifications as read
responses:
202:
description: "accepted"
403:
description: "unauthorized"
/1.0/main/notification/mark/undoallread:
post: # Use POST method for creating resources
tags:
- notification
summary: Mark notifications as unread
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
ids:
type: array
items:
type: integer
example: [1, 2, 3] # Example array of IDs
responses:
"202":
description: Notifications marked as unread successfully
"403":
description: Unauthorized
/1.0/main/civility.json: /1.0/main/civility.json:
get: get:
tags: tags:
- civility - civility
summary: Return all civility types summary: Return all civility types
responses: responses:
200: 200:
@ -844,7 +873,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/UserJob' $ref: "#/components/schemas/UserJob"
/1.0/main/workflow/my: /1.0/main/workflow/my:
get: get:
tags: tags:
@ -858,7 +887,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: '#/components/schemas/Workflow' $ref: "#/components/schemas/Workflow"
403: 403:
description: "Unauthorized" description: "Unauthorized"
/1.0/main/workflow/my-cc: /1.0/main/workflow/my-cc:
@ -874,7 +903,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: '#/components/schemas/Workflow' $ref: "#/components/schemas/Workflow"
403: 403:
description: "Unauthorized" description: "Unauthorized"
/1.0/main/dashboard-config-item.json: /1.0/main/dashboard-config-item.json:
@ -888,7 +917,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/DashboardConfigItem' $ref: "#/components/schemas/DashboardConfigItem"
403: 403:
description: "Unauthorized" description: "Unauthorized"
@ -905,6 +934,6 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: '#/components/schemas/NewsItem' $ref: "#/components/schemas/NewsItem"
403: 403:
description: "Unauthorized" description: "Unauthorized"

View File

@ -70,6 +70,7 @@ module.exports = function(encore, entries)
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js'); encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js'); encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
encore.addEntry('mod_notification_toggle_read_status', __dirname + '/Resources/public/module/notification/toggle_read.js'); encore.addEntry('mod_notification_toggle_read_status', __dirname + '/Resources/public/module/notification/toggle_read.js');
encore.addEntry('mod_notification_toggle_read_all_status', __dirname + '/Resources/public/module/notification/toggle_read_all.ts');
encore.addEntry('mod_pickentity_type', __dirname + '/Resources/public/module/pick-entity/index.js'); encore.addEntry('mod_pickentity_type', __dirname + '/Resources/public/module/pick-entity/index.js');
encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js'); encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js');
encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js'); encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js');