Merge branch 'notifications'

This commit is contained in:
isUnknown 2024-12-19 11:12:11 +01:00
commit caf6f721fb
34 changed files with 671 additions and 417 deletions

View file

@ -6,112 +6,58 @@ Comments:
uri: > uri: >
projects/miss-dior-blooming-bouquet/proposal projects/miss-dior-blooming-bouquet/proposal
title: Offre commerciale title: Offre commerciale
href: '/projects/miss-dior-blooming-bouquet?dialog=proposal&fileIndex=0'
project: project:
title: Miss Dior Blooming Bouquet title: Miss Dior Blooming Bouquet
uri: projects/miss-dior-blooming-bouquet uri: projects/miss-dior-blooming-bouquet
dialogUri: '/projects/miss-dior-blooming-bouquet?dialog=proposal&fileIndex=0'
file: file:
uuid: file://3vTh1tMFeFM2JxaN uuid: file://3vTh1tMFeFM2JxaN
url: file://3vTh1tMFeFM2JxaN url: >
http://localhost:8888/media/pages/projects/miss-dior-blooming-bouquet/proposal/788ddebfe3-1731941917/des-textos-revelent-comment-bfm-sest-mise-au-service-de-sarkozy-le-boss.pdf
position: position:
pageIndex: 1 pageIndex: 1
x: "60.110185093015" x: "57.105131038425"
y: "44.594594594595" y: "13.127413127413"
replies: [ ] text: un commentaire
text: test
author: author:
name: Adrien Payet name: Adrien Payet
email: adrien.payet@outlook.com email: adrien.payet@outlook.com
uuid: user://WWjXgPWk uuid: user://WWjXgPWk
role: admin role: admin
date: 2024-11-28T16:30:56+01:00 date: 2024-12-19T10:26:05+01:00
id: m41h238q id: e16a5304-330b-4460-acbd-a6e471f4cda2
type: comment type: comment
isRead: false replies:
- -
location: location:
page: page:
uri: > uri: >
projects/miss-dior-blooming-bouquet/proposal projects/miss-dior-blooming-bouquet/proposal
title: Offre commerciale title: Offre commerciale
href: '/projects/miss-dior-blooming-bouquet?dialog=proposal&fileIndex=0' project:
project: title: Miss Dior Blooming Bouquet
title: Miss Dior Blooming Bouquet uri: projects/miss-dior-blooming-bouquet
uri: projects/miss-dior-blooming-bouquet dialogUri: '/projects/miss-dior-blooming-bouquet?dialog=proposal&fileIndex=0'
file: file:
uuid: file://3vTh1tMFeFM2JxaN uuid: file://3vTh1tMFeFM2JxaN
url: file://3vTh1tMFeFM2JxaN url: >
position: http://localhost:8888/media/pages/projects/miss-dior-blooming-bouquet/proposal/788ddebfe3-1731941917/des-textos-revelent-comment-bfm-sest-mise-au-service-de-sarkozy-le-boss.pdf
pageIndex: 1 parent:
x: "26.098441098196" author:
y: "64.864864864865" name: Adrien Payet
replies: [ ] email: adrien.payet@outlook.com
text: Nouveau commentaire id: e16a5304-330b-4460-acbd-a6e471f4cda2
author: position:
name: Utilisateur Dior pageIndex: 1
email: utilisateur@dior.com text: une réponse
uuid: user://HfuumN8s author:
role: client name: Adrien Payet
date: 2024-12-04T15:00:15+01:00 email: adrien.payet@outlook.com
id: m49ygks2 uuid: user://WWjXgPWk
type: comment role: admin
isRead: false date: 2024-12-19T10:26:09+01:00
- id: bc64e066-299d-4d80-a651-2c17482cda2f
location: type: comment-reply
page:
uri: >
projects/miss-dior-blooming-bouquet/proposal
title: Offre commerciale
href: '/projects/miss-dior-blooming-bouquet?dialog=proposal&fileIndex=0'
project:
title: Miss Dior Blooming Bouquet
uri: projects/miss-dior-blooming-bouquet
file:
uuid: file://3vTh1tMFeFM2JxaN
url: file://3vTh1tMFeFM2JxaN
position:
pageIndex: 1
x: "64.481172808783"
y: "76.447876447876"
replies: [ ]
text: encore un commentaire
author:
name: Utilisateur Dior
email: utilisateur@dior.com
uuid: user://HfuumN8s
role: client
date: 2024-12-04T15:03:53+01:00
id: m49yl8zc
type: comment
isRead: false
-
location:
page:
uri: >
projects/miss-dior-blooming-bouquet/proposal
title: Offre commerciale
href: '/projects/miss-dior-blooming-bouquet?dialog=proposal&fileIndex=0'
project:
title: Miss Dior Blooming Bouquet
uri: projects/miss-dior-blooming-bouquet
file:
uuid: file://3vTh1tMFeFM2JxaN
url: file://3vTh1tMFeFM2JxaN
position:
pageIndex: 1
x: '29.786457814876'
y: '83.397683397683'
replies: [ ]
text: et encore un
author:
name: Utilisateur Dior
email: utilisateur@dior.com
uuid: user://HfuumN8s
role: client
date: 2024-12-04T15:05:10+01:00
id: m49ymwuw
type: comment
isRead: false
---- ----

View file

@ -26,7 +26,7 @@ Comments:
date: 2024-12-03T13:42:18+01:00 date: 2024-12-03T13:42:18+01:00
id: m48g8hrb id: m48g8hrb
type: comment type: comment
isRead: false isread: false
- -
location: location:
page: page:
@ -53,7 +53,7 @@ Comments:
date: 2024-12-03T13:43:08+01:00 date: 2024-12-03T13:43:08+01:00
id: m48g9kau id: m48g9kau
type: comment type: comment
isRead: false isread: false
---- ----

View file

@ -4,4 +4,48 @@ home: /panel/pages/projects
fields: fields:
notifications: notifications:
type: hidden label: Notifications
type: structure
fields:
type:
type: text
disabled: true
location:
type: object
fields:
page:
type: object
fields:
label: Page d'origine
disabled: true
title:
type: text
uri:
disabled: true
type: text
project:
label: Projet correspondant
type: object
fields:
title:
disabled: true
type: text
uri:
disabled: true
type: text
author:
label: Auteur
type: object
fields:
name:
label: Nom
disabled: true
type: text
email:
type: email
disabled: true
date:
type: hidden
isread:
type: toggle
disabled: true

View file

@ -7,12 +7,12 @@ return [
$json = file_get_contents('php://input'); $json = file_get_contents('php://input');
$data = json_decode($json); $data = json_decode($json);
$brief = page($data->briefUri); $page = page($data->briefUri);
$project = $brief->parent(); $project = $page->parent();
$href = $data->dialogUri ? $data->dialogUri : $brief->url(); $dialogUri = $data->dialogUri ? $data->dialogUri : null;
try { try {
$newPage = $brief->update([ $newPage = $page->update([
'isValidated' => 'true' 'isValidated' => 'true'
]); ]);
@ -21,20 +21,13 @@ return [
$notification = [ $notification = [
'location' => [ 'location' => [
'href' => (string) $href, 'page' => $page,
'project' => [ 'project' => $project,
'title' => (string) $project->title(), 'dialogUri' => (string) $dialogUri,
'uri' => (string) $project->url()
]
], ],
'date' => $dateTime->format('Y-m-d\TH:i:sP'), 'date' => $dateTime->format('Y-m-d\TH:i:sP'),
'text' => "Brief (" . $project->title()->value() . ")", 'text' => "Nouveau brief",
'author' => [ 'author' => kirby()->user(),
'name' => (string) kirby()->user()->name(),
'email' => (string) kirby()->user()->email(),
'uuid' => (string) kirby()->user()->uuid(),
'role' => (string) kirby()->user()->role(),
],
'id' => Str::uuid(), 'id' => Str::uuid(),
'type' => 'content' 'type' => 'content'
]; ];
@ -46,7 +39,7 @@ return [
]; ];
} catch (\Throwable $th) { } catch (\Throwable $th) {
return [ return [
"error" => "Can't validate '" . $brief->title()->value() . "' brief.", "error" => "Can't validate '" . $page->title()->value() . "' brief.",
'details' => $th->getMessage() 'details' => $th->getMessage()
]; ];
} }

View file

@ -0,0 +1,27 @@
<?php
namespace adrienpayet\D2P\data;
class Author
{
protected string $name;
protected string $email;
protected string $uuid;
protected string $role;
public function __construct($author) {
$this->name = (string) $author->name();
$this->email = (string) $author->email();
$this->uuid = (string) $author->uuid();
$this->role = (string) $author->role();
}
public function toArray() {
return [
"name" => $this->name,
"email" => $this->email,
"uuid" => $this->uuid,
"role" => $this->role,
];
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace adrienpayet\D2P\data;
class Position
{
public int $pageIndex;
public ?float $x = null;
public ?float $y = null;
public function __construct(array $data)
{
$this->pageIndex = $data['pageIndex'];
if (isset($data["x"])) {
$this->x = (float) $data['x'];
$this->y = (float) $data['y'];
}
}
public function toArray() {
$array = [
"pageIndex" => $this->pageIndex,
];
if ($this->x) {
$array["x"] = $this->x;
$array["y"] = $this->y;
}
return $array;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace adrienpayet\D2P\data\location;
use Kirby;
class FileDetails
{
public string $uuid;
public string $url;
public function __construct(Kirby\Cms\File $data)
{
$this->uuid = (string) $data->uuid();
$this->url = (string) $data->url();
}
public function toArray() {
return [
"uuid" => $this->uuid,
"url" => $this->url
];
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace adrienpayet\D2P\data\location;
class Location
{
protected PageDetails $page;
protected ?string $dialogUri = null;
protected ProjectDetails $project;
protected ?FileDetails $file = null;
protected ?array $parent = null;
public function __construct(array $data)
{
$this->page = new PageDetails($data["page"]);
$this->dialogUri = $data["dialogUri"];
$this->project = new ProjectDetails($data["project"]);
if (isset($data['file'])) {
$this->file = new FileDetails($data["file"]);
}
if (isset($data["parent"])) {
$this->parent = [
"author" => [
"name" => $data["parent"]["author"]["name"],
"email" => $data["parent"]["author"]["email"],
],
"id" => $data["parent"]["id"]
];
}
}
public function toArray() {
$array = [
"page" => $this->page->toArray(),
"project" => $this->project->toArray(),
];
if ($this->dialogUri) {
$array["dialogUri"] = $this->dialogUri;
}
if ($this->file) {
$array["file"] = $this->file;
}
if ($this->parent) {
$array["parent"] = $this->parent;
}
return $array;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace adrienpayet\D2P\data\location;
use Kirby\Cms\Page;
class PageDetails
{
protected Page $page;
public function __construct(Page $page)
{
$this->page = $page;
}
public function toArray() {
return [
"uri" => (string) $this->page->uri(),
"title" => (string) $this->page->title(),
];
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace adrienpayet\D2P\data\location;
use Kirby;
class ProjectDetails
{
public string $title;
public string $uri;
public function __construct(Kirby\Cms\Page $page)
{
$this->title = (string) $page->title();
$this->uri = (string) $page->uri();
}
public function toArray() {
return [
"title" => $this->title,
"uri" => $this->uri
];
}
}

View file

@ -3,7 +3,14 @@
F::loadClasses([ F::loadClasses([
'adrienpayet\\comments\\BaseComment' => __DIR__ . '/src/BaseComment.php', 'adrienpayet\\comments\\BaseComment' => __DIR__ . '/src/BaseComment.php',
'adrienpayet\\comments\\Comment' => __DIR__ . '/src/Comment.php', 'adrienpayet\\comments\\Comment' => __DIR__ . '/src/Comment.php',
'adrienpayet\\comments\\Reply' => __DIR__ . '/src/Reply.php' 'adrienpayet\\comments\\Reply' => __DIR__ . '/src/Reply.php',
'adrienpayet\\D2P\\data\\Position' => __DIR__ . '/../classes/Position.php',
'adrienpayet\\D2P\data\Author' => __DIR__ . '/../classes/Author.php',
'adrienpayet\\D2P\\data\\location\\Location' => __DIR__ . '/../classes/location/Location.php',
'adrienpayet\\D2P\\data\\location\\PageDetails' => __DIR__ . '/../classes/location/PageDetails.php',
'adrienpayet\\D2P\\data\\location\\ProjectDetails' => __DIR__ . '/../classes/location/ProjectDetails.php',
'adrienpayet\\D2P\\data\\location\\FileDetails' => __DIR__ . '/../classes/location/FileDetails.php',
]); ]);
Kirby::plugin('adrienpayet/kirby4-comments', [ Kirby::plugin('adrienpayet/kirby4-comments', [

View file

@ -9,49 +9,51 @@ return [
$json = file_get_contents('php://input'); $json = file_get_contents('php://input');
$data = json_decode($json); $data = json_decode($json);
$parsedUrl = parse_url($data->path); $parsedUrl = parse_url($data->dialogUri);
$query = $parsedUrl['query'] ?? null; $query = $parsedUrl['query'] ?? null;
parse_str($query, $queryParams); parse_str($query, $queryParams);
$stepSlug = $queryParams['dialog'] ?? null; $stepSlug = $queryParams['dialog'] ?? null;
$targetPageUri = $stepSlug ? $parsedUrl['path'] . '/' . $stepSlug : $parsedUrl['path']; $targetPageUri = $stepSlug ? $parsedUrl['path'] . '/' . $stepSlug : $parsedUrl['path'];
$project = page($parsedUrl['path']);
$page = page($targetPageUri); $page = page($targetPageUri);
$file = $page->file($data->fileName); $file = $page->file($data->fileName);
$user = kirby()->user($data->userUuid); $user = kirby()->user($data->userUuid);
$comments = $file->comments()->isEmpty() == true ? [] : Yaml::decode($file->comments()->value()); $comments = $file->comments()->isEmpty() == true ? [] : Yaml::decode($file->comments()->value());
$data = [ $commentData = [
'href' => $data->path, "location" => [
'page' => $page, "page" => $page,
'file' => $file, "project" => $project,
'position' => [ "dialogUri" => $data->dialogUri,
'pageIndex' => $data->position->pageIndex, "file" => $file
'x' => $data->position->x,
'y' => $data->position->y
], ],
'replies' => [], "position" => [
'text' => $data->text, "pageIndex" => $data->position->pageIndex,
'author' => $user, "x" => $data->position->x,
'date' => (string) $data->date, "y" => $data->position->y
'id' => $data->id, ],
'type' => 'comment' "date" => (string) $data->date,
"text" => $data->text,
"author" => kirby()->user(),
"id" => Str::uuid(),
"type" => "comment",
]; ];
$newComment = new Comment($data); $newComment = new Comment($commentData);
$comments[] = $newComment->toArray(); $comments[] = $newComment->toArray();
$newFile = $file->update([ $newFile = $file->update([
'comments' => $comments 'comments' => $comments
]); ]);
echo json_encode(getFileData($newFile)); echo json_encode(getFileData($newFile));
try { try {
$user->sendNotification($page->parent(), $newComment->toArray()); $user->sendNotification($project, $commentData);
} catch (\Throwable $th) { } catch (\Throwable $th) {
echo json_encode([ echo json_encode([
"error" => $th->getMessage() . " in " . $th->getFile() . " line " . $th->getLine(), "error" => $th->getMessage() . " in " . $th->getFile() . " line " . $th->getLine(),

View file

@ -10,13 +10,13 @@ return [
$page = page($data->location->page->uri); $page = page($data->location->page->uri);
$project = page($data->location->project->uri); $project = page($data->location->project->uri);
$file = $page->file($data->location->file->uuid); $file = $page->file($data->location->file->uuid);
$isReply = $data->parentId ?? false; $isReply = $data->location->parentId ?? false;
$comments = $file->comments()->isEmpty() == true ? [] : Yaml::decode($file->comments()->value()); $comments = $file->comments()->isEmpty() == true ? [] : Yaml::decode($file->comments()->value());
foreach ($comments as $key => &$comment) { foreach ($comments as $key => &$comment) {
if ($isReply) { if ($isReply) {
if ($comment['id'] === $data->parentId) { if ($comment['id'] === $data->location->parentId) {
foreach ($comment['replies'] as $replyKey => $reply) { foreach ($comment['replies'] as $replyKey => $reply) {
if ($reply['id'] === $data->id) { if ($reply['id'] === $data->id) {
unset($comment['replies'][$replyKey]); unset($comment['replies'][$replyKey]);

View file

@ -9,39 +9,42 @@ return [
$json = file_get_contents('php://input'); $json = file_get_contents('php://input');
$data = json_decode($json); $data = json_decode($json);
$parsedUrl = parse_url($data->path); $parsedUrl = parse_url($data->dialogUri);
$query = $parsedUrl['query'] ?? null; $query = $parsedUrl['query'] ?? null;
parse_str($query, $queryParams); parse_str($query, $queryParams);
$stepSlug = $queryParams['dialog'] ?? null; $stepSlug = $queryParams['dialog'] ?? null;
$targetPageUri = $stepSlug ? $parsedUrl['path'] . '/' . $stepSlug : $parsedUrl['path']; $targetPageUri = $stepSlug ? $parsedUrl['path'] . '/' . $stepSlug : $parsedUrl['path'];
$project = page($parsedUrl['path']);
$page = page($targetPageUri); $page = page($targetPageUri);
$file = $page->file($data->fileName); $file = $page->file($data->fileName);
$user = kirby()->user($data->userUuid); $user = kirby()->user($data->userUuid);
$comments = $file->comments()->isEmpty() == true ? [] : Yaml::decode($file->comments()->value()); $comments = $file->comments()->isEmpty() == true ? [] : Yaml::decode($file->comments()->value());
$data = [
'href' => $data->path,
'page' => $page,
'parentId' => $data->parentId,
'file' => $file,
'position' => [
'pageIndex' => $data->position->pageIndex,
],
'text' => $data->text,
'author' => $user,
'date' => (string) $data->date,
'id' => $data->id,
'type' => 'comment'
];
$newReply = new Reply($data);
foreach ($comments as &$comment) { foreach ($comments as &$comment) {
if ($comment['id'] === $newReply->parentId()) { $isParentComment = $comment['id'] === $data->parentId;
if ($isParentComment) {
$replyData = [
"location" => [
"page" => $page,
"project" => $project,
"dialogUri" => $data->dialogUri,
"file" => $file,
"parent" => $comment
],
"position" => [
"pageIndex" => $data->position->pageIndex,
],
"date" => (string) $data->date,
"text" => $data->text,
"author" => kirby()->user(),
"id" => Str::uuid(),
"type" => "comment-reply",
];
$newReply = new Reply($replyData);
$comment['replies'][] = $newReply->toArray(); $comment['replies'][] = $newReply->toArray();
} }
} }
@ -50,7 +53,7 @@ return [
'comments' => $comments 'comments' => $comments
]); ]);
$user->sendNotification($page->parent(), $newReply->toArray()); $user->sendNotification($project, $replyData);
return getFileData($newFile); return getFileData($newFile);
} }

View file

@ -1,123 +1,35 @@
<?php <?php
namespace adrienpayet\comments; namespace adrienpayet\comments;
use adrienpayet\D2P\data\location\Location;
use adrienpayet\D2P\data\Author;
use adrienpayet\D2P\data\Position;
class BaseComment { class BaseComment {
protected $href; protected Location $location;
protected $location; protected Author $author;
protected $position; protected string $text;
protected $replies; protected string $date;
protected $text; protected string $id;
protected $author; protected Position $position;
protected $date;
protected $id;
protected $type;
protected $isRead;
public function __construct($data) { public function __construct($data) {
$page = $data['page']; $this->location = new Location($data["location"]);
$project = $page->template() == 'project' ? $page : $page->parent(); $this->text = $data["text"];
$file = $data['file']; $this->author = new Author($data["author"]);
$position = $data['position']; $this->date = $data["date"];
$replies = $data['replies'] ?? []; $this->id = $data["id"];
$text = $data['text']; $this->position = new Position($data['position']);
$author = $data['author'];
$date = $data['date'];
$id = $data['id'];
$type = $data['type'] ?? 'comment';
$this->location = [
'page' => [
'uri' => (string) $page->uri(),
'title' => (string) $page->title(),
],
'href' => (string) $data['href'],
'project' => [
'title' => (string) $project->title(),
'uri' => (string) $project->uri(),
],
'file' => $file ? [
'uuid' => (string) $file->uuid(),
'url' => (string) $file->uuid()
] : false,
];
$this->replies = $replies ?? [];
$this->text = $text;
$this->author = [
'name' => (string) $author->name(),
'email' => (string) $author->email(),
'uuid' => (string) $author->uuid(),
'role' => (string) $author->role(),
];
$this->date = $date;
$this->id = $id;
$this->type = $type;
$this->isRead = false;
$this->position = $position;
}
public function location() {
return $this->location;
}
public function file() {
return $this->location['file'];
}
public function replies() {
return $this->replies;
}
public function text() {
return $this->text;
}
public function author() {
return $this->author;
}
public function date() {
return $this->date;
}
public function id() {
return $this->id;
}
public function type() {
return $this->type;
}
public function isRead() {
return $this->isRead;
}
public function read() {
$this->isRead = true;
return $this->isRead;
} }
public function unread() {
$this->isRead = false;
return $this->isRead;
}
public function pageIndex() {
$this->position['pageIndex'];
}
public function toArray() { public function toArray() {
return [ return [
'location' => $this->location, "location" => $this->location->toArray(),
'position' => $this->position, "position" => $this->position->toArray(),
'replies' => $this->replies, "text" => $this->text,
'text' => $this->text, "author" => $this->author->toArray(),
'author' => $this->author, "date" => $this->date,
'date' => $this->date, "id" => $this->id,
'id' => $this->id,
'type' => $this->type,
'isRead' => $this->isRead
]; ];
} }
} }

View file

@ -4,20 +4,17 @@ namespace adrienpayet\comments;
class Comment extends BaseComment class Comment extends BaseComment
{ {
protected $position; protected string $type = "comment";
protected array $replies = [];
public function __construct($data) { public function __construct($data) {
parent::__construct($data); parent::__construct($data);
$this->position = $data['position'] ?? null;
}
public function position() {
return $this->position;
} }
public function toArray() { public function toArray() {
$array = parent::toArray(); $array = parent::toArray();
$array['position'] = $this->position; $array['type'] = $this->type;
$array['replies'] = $this->replies;
return $array; return $array;
} }

View file

@ -3,21 +3,15 @@ namespace adrienpayet\comments;
class Reply extends BaseComment { class Reply extends BaseComment {
protected $parentId; protected string $type = "comment-reply";
public function __construct($data) { public function __construct($data) {
parent::__construct($data); parent::__construct($data);
$this->parentId = $data['parentId'];
}
public function parentId() {
return $this->parentId;
} }
public function toArray() { public function toArray() {
$array = parent::toArray(); $array = parent::toArray();
$array['parentId'] = $this->parentId; $array['type'] = $this->type;
return $array; return $array;
} }

View file

@ -4,6 +4,17 @@ load([
'ProjectPage' => 'models/ProjectPage.php', 'ProjectPage' => 'models/ProjectPage.php',
], __DIR__); ], __DIR__);
F::loadClasses([
'adrienpayet\\notifications\\Notification' => __DIR__ . '/src/Notification.php',
'adrienpayet\\D2P\\data\\Position' => __DIR__ . '/../classes/Position.php',
'adrienpayet\\D2P\data\Author' => __DIR__ . '/../classes/Author.php',
'adrienpayet\\D2P\\data\\location\\Location' => __DIR__ . '/../classes/location/Location.php',
'adrienpayet\\D2P\\data\\location\\PageDetails' => __DIR__ . '/../classes/location/PageDetails.php',
'adrienpayet\\D2P\\data\\location\\ProjectDetails' => __DIR__ . '/../classes/location/ProjectDetails.php',
'adrienpayet\\D2P\\data\\location\\FileDetails' => __DIR__ . '/../classes/location/FileDetails.php',
]);
Kirby::plugin('adrienpayet/pdc-notifications', [ Kirby::plugin('adrienpayet/pdc-notifications', [
'routes' => [ 'routes' => [
require(__DIR__ . '/routes/readAll.php'), require(__DIR__ . '/routes/readAll.php'),

View file

@ -0,0 +1,42 @@
<?php
namespace adrienpayet\notifications;
use adrienpayet\D2P\data\location\Location;
use adrienpayet\D2P\data\Author;
class Notification
{
protected string $type;
protected Location $location;
protected string $text;
protected Author $author;
protected string $date;
protected string $id;
protected string $isread = "false";
protected ?Position $position = null;
public function __construct($data) {
$this->type = $data["type"];
$this->location = new Location($data["location"]);
$this->text = $data["text"];
$this->author = new Author($data["author"]);
$this->date = $data["date"];
$this->id = $data["id"];
}
public function toArray() {
$array = [
"type" => $this->type,
"location" => $this->location->toArray(),
"text" => $this->text,
"author" => $this->author->toArray(),
"date" => $this->date,
"id" => $this->id,
"isread" => $this->isread,
];
return $array;
}
}

View file

@ -10,7 +10,7 @@ return function($notificationId) {
foreach ($notifications as $key => $notification) { foreach ($notifications as $key => $notification) {
if (!isset($notification['id'])) continue; if (!isset($notification['id'])) continue;
if ($notification['id'] === $notificationId) { if ($notification['id'] === $notificationId) {
$notifications[$key]['isRead'] = true; $notifications[$key]['isread'] = "true";
$newNotification = $notifications[$key]; $newNotification = $notifications[$key];
} }
} }

View file

@ -8,7 +8,7 @@ return function() {
$newNotification = null; $newNotification = null;
foreach ($notifications as $key => $notification) { foreach ($notifications as $key => $notification) {
$notifications[$key]['isRead'] = true; $notifications[$key]['isread'] = true;
} }
$updatedUser = $this->update([ $updatedUser = $this->update([

View file

@ -1,5 +1,7 @@
<?php <?php
use adrienpayet\notifications\Notification;
/** /**
* Send a notification to all users who manage a specific project, excluding the user sending the notification. * Send a notification to all users who manage a specific project, excluding the user sending the notification.
* *
@ -14,26 +16,26 @@
*/ */
return function ($project, $notificationData) { return function ($project, $notificationData) {
$recipients = $project->managers()->without($this); $recipients = $project->managers()->without($this);
if ($recipients->isEmpty()) return; if ($recipients->isEmpty()) return;
$notificationData['isRead'] = false; $newNotification = new Notification($notificationData);
foreach ($recipients as $otherUser) { foreach ($recipients as $otherUser) {
try { try {
$notifications = $otherUser->notifications()->isNotEmpty() $notifications = $otherUser->notifications()->isNotEmpty()
? Yaml::decode($otherUser->notifications()->value()) ? Yaml::decode($otherUser->notifications()->value())
: []; : [];
$notifications[] = $notificationData; $notifications[] = $newNotification->toArray();
$otherUser->update([ $otherUser->update([
'notifications' => $notifications 'notifications' => $notifications
]); ]);
} catch (\Throwable $th) { } catch (\Throwable $th) {
error_log("Notification error for user " . $otherUser->email() . ": " . $th->getMessage()); error_log("Notification error for user " . $otherUser->email() . ": " . $th->getMessage());
throw new Exception("Error updating notifications: " . $th->getMessage() . ' line ' . $th->getLine(), 1); throw new Exception("Error updating notifications: " . $th->getMessage() . ' line ' . $th->getLine(), 1);
} }
} }
}; };

View file

@ -117,7 +117,7 @@ const { page } = storeToRefs(usePageStore());
const unreadNotificationsCount = computed(() => { const unreadNotificationsCount = computed(() => {
if (!user.value) return undefined; if (!user.value) return undefined;
const count = user.value.notifications.filter( const count = user.value.notifications.filter(
(notification) => notification.isRead (notification) => notification.isread != "true"
).length; ).length;
if (count === 0) return undefined; if (count === 0) return undefined;
return count; return count;
@ -164,7 +164,7 @@ function hasUnreadNotification(project) {
if (!user.value) return false; if (!user.value) return false;
return user.value.notifications.some((notification) => { return user.value.notifications.some((notification) => {
return ( return (
notification.isRead != true && notification.isread != "true" &&
project.uri.includes(notification.location.project.uri) project.uri.includes(notification.location.project.uri)
); );
}); });

View file

@ -2,7 +2,7 @@
<article <article
:id="`comment-${comment.id}`" :id="`comment-${comment.id}`"
class="comment | flow" class="comment | flow"
:data-status="status" :data-status="getStatus"
@click="read()" @click="read()"
> >
<header> <header>
@ -16,7 +16,7 @@
<time <time
class="comment__date" class="comment__date"
:datetime="dayjs(comment.date).format('YYYY-MM-DD')" :datetime="dayjs(comment.date).format('YYYY-MM-DD')"
>{{ formatDate() }}</time >{{ formatDate(comment.date) }}</time
> >
</p> </p>
</header> </header>
@ -70,11 +70,11 @@ const api = useApiStore();
const dialog = useDialogStore(); const dialog = useDialogStore();
// Functions // Functions
const status = computed(() => { const getStatus = computed(() => {
const correspondingNotification = userStore.notifications.find( const correspondingNotification = userStore.notifications.find(
(notification) => notification.id === comment.id (notification) => notification.id === comment.id
); );
if (correspondingNotification && !correspondingNotification.isRead) { if (correspondingNotification && correspondingNotification.isread != "true") {
return "unread"; return "unread";
} }
return undefined; return undefined;
@ -101,9 +101,10 @@ function closeAddField() {
} }
async function read() { async function read() {
if (status.value !== "unread") return; if (getStatus.value !== "unread") return;
try { try {
const newNotification = await api.readNotification(comment.id); const newNotification = await api.readNotification(comment.id);
console.log(newNotification);
userStore.readNotification(comment.id); userStore.readNotification(comment.id);
} catch (error) { } catch (error) {
console.log("Erreur lors de la lecture de la notification : ", error); console.log("Erreur lors de la lecture de la notification : ", error);
@ -112,9 +113,10 @@ async function read() {
async function deleteComment(event) { async function deleteComment(event) {
event.stopPropagation(); event.stopPropagation();
console.log(comment);
const newFile = await api.deleteComment(comment); const newFile = await api.deleteComment(comment);
dialog.updateFile(newFile); dialog.updateFile(newFile);
if (comment.parentId) { if (comment.type === "comment-reply") {
emits("close:comment"); emits("close:comment");
} }
} }

View file

@ -178,7 +178,7 @@ function handleSubmit(event = null) {
} }
const date = dayjs().format(); const date = dayjs().format();
const newComment = { const newComment = {
path: route.fullPath, dialogUri: route.fullPath,
fileName: openedFile ? openedFile.value.name : false, fileName: openedFile ? openedFile.value.name : false,
userUuid: user.uuid, userUuid: user.uuid,
text: draftComment.value.text, text: draftComment.value.text,

View file

@ -0,0 +1,46 @@
<template>
<article
class="notification | bg-white rounded-lg | p-16 | flow"
:data-status="notification.isread == 'true' ? 'read' : 'unread'"
data-type="comment"
@click="router.push(notification.location.dialoguri + '&comments=true')"
>
<header>
<p class="flex">
<strong
class="notification__type | font-medium text-primary"
data-icon="comment"
>Commentaire</strong
>
<span class="notification__client | text-grey-700">{{
notification.location.project.title
}}</span>
<time
datetime=""
class="notification__date | text-grey-700 | ml-auto"
>{{ formatDate(notification) }}</time
>
</p>
</header>
<p
v-if="notification.type"
class="notification__body | text-md font-medium | line-clamp"
>
{{
notification.author.name
? notification.author.name
: notification.author.email
}}
: {{ notification.text }}
</p>
</article>
</template>
<script setup>
import { useRouter } from "vue-router";
import { useNotificationsStore } from "../../stores/notifications";
const router = useRouter();
const { notification } = defineProps({ notification: Object });
const { formatDate } = useNotificationsStore();
</script>

View file

@ -0,0 +1,45 @@
<template>
<article
class="notification | bg-white rounded-lg | p-16 | flow"
:data-status="notification.isread == 'true' ? 'read' : 'unread'"
data-type="content"
@click="
read(notification);
router.push(notification.location.page.uri);
"
title="Aller au contenu"
>
<header>
<p class="flex">
<strong
class="notification__type | font-medium text-primary"
data-icon="content"
>Contenu</strong
>
<span class="notification__client | text-grey-700">{{
notification.location.project.title
}}</span>
<time
datetime=""
class="notification__date | text-grey-700 | ml-auto"
>{{ formatDate(notification) }}</time
>
</p>
</header>
<p
v-if="notification.type"
class="notification__body | text-md font-medium | line-clamp"
>
{{ notification.text }}
</p>
</article>
</template>
<script setup>
import { useRouter } from "vue-router";
import { useNotificationsStore } from "../../stores/notifications";
const router = useRouter();
const { notification } = defineProps({ notification: Object });
const { formatDate, read } = useNotificationsStore();
</script>

View file

@ -0,0 +1,51 @@
<template>
<article
class="notification | bg-white rounded-lg | p-16 | flow"
:data-status="notification.isread == 'true' ? 'read' : 'unread'"
data-type="comment"
@click="router.push(notification.location.dialoguri + '&comments=true')"
>
<header>
<p class="flex">
<strong
class="notification__type | font-medium text-primary"
data-icon="comment"
>Réponse à
{{
notification.location.parent.author.name
? notification.location.parent.author.name
: notification.location.parent.author.email
}}</strong
>
<span class="notification__client | text-grey-700">{{
notification.location.project.title
}}</span>
<time
datetime=""
class="notification__date | text-grey-700 | ml-auto"
>{{ formatDate(notification) }}</time
>
</p>
</header>
<p
v-if="notification.type"
class="notification__body | text-md font-medium | line-clamp"
>
{{
notification.author.name
? notification.author.name
: notification.author.email
}}
: {{ notification.text }}
</p>
</article>
</template>
<script setup>
import { useRouter } from "vue-router";
import { useNotificationsStore } from "../../stores/notifications";
const router = useRouter();
const { notification } = defineProps({ notification: Object });
const { formatDate } = useNotificationsStore();
</script>

View file

@ -36,6 +36,11 @@ const draftComment = ref(null);
const isViewerDisabled = ref(false); const isViewerDisabled = ref(false);
watch(openedFile, () => {
removeCommentMarkers();
setCommentMarkers();
});
watch(isCommentsOpen, (newVal) => { watch(isCommentsOpen, (newVal) => {
if (newVal) { if (newVal) {
setCommentMarkers(); setCommentMarkers();
@ -45,6 +50,8 @@ watch(isCommentsOpen, (newVal) => {
}); });
watch(openedFile, (newVal) => { watch(openedFile, (newVal) => {
if (!location.href.includes("virtual-sample")) return;
isViewerDisabled.value = true; isViewerDisabled.value = true;
setTimeout(() => { setTimeout(() => {

View file

@ -223,7 +223,6 @@ function getFrontView(track) {
const extension = track.files[0].name.split(".")[1]; const extension = track.files[0].name.split(".")[1];
const frontViewName = "0_" + xFrontView + "." + extension; const frontViewName = "0_" + xFrontView + "." + extension;
const frontView = track.files.find((file) => file.name === frontViewName); const frontView = track.files.find((file) => file.name === frontViewName);
console.log(frontView);
return frontView; return frontView;
} }
</script> </script>

View file

@ -0,0 +1,45 @@
import { defineStore } from "pinia";
import dayjs from "dayjs";
import "dayjs/locale/fr";
import { useRouter } from "vue-router";
import { useApiStore } from "./api";
export const useNotificationsStore = defineStore("notifications", () => {
dayjs.locale("fr");
const router = useRouter();
const api = useApiStore();
function formatDate(notification) {
const notificationDigitalDate = parseInt(
dayjs(notification.date).format("YYYYMMDD")
);
const todayDigitalDate = parseInt(dayjs().format("YYYYMMDD"));
const isToday = notificationDigitalDate === todayDigitalDate;
if (isToday) {
return dayjs(notification.date).format("HH:mm");
} else if (todayDigitalDate === notificationDigitalDate + 1) {
return "hier";
} else {
return dayjs(notification.date).format("DD MMM YY");
}
}
function read(notification) {
if (!notification.id) {
console.error(
"Couldn't change notification status because it has no id."
);
} else if (notification.isread != true) {
api
.readNotification(notification.id)
.then((res) => {
console.log(res);
})
.catch((err) => console.error("Notification could not be read.", err));
}
}
return { formatDate, read };
});

View file

@ -13,14 +13,14 @@ export const useUserStore = defineStore("user", () => {
function readNotification(notificationId) { function readNotification(notificationId) {
user.value.notifications.forEach((notification) => { user.value.notifications.forEach((notification) => {
if (notification.id === notificationId) { if (notification.id === notificationId) {
notification.isRead = true; notification.isread = "true";
} }
}); });
} }
function readAllNotifications(notificationId) { function readAllNotifications(notificationId) {
user.value.notifications.forEach((notification) => { user.value.notifications.forEach((notification) => {
notification.isRead = true; notification.isread = true;
}); });
} }

View file

@ -6,6 +6,10 @@ function toPascalCase(string) {
}); });
} }
const StringUtils = { toPascalCase }; function urlToPath(url) {
return url.replace(window.location.origin, "");
}
const StringUtils = { toPascalCase, urlToPath };
export default StringUtils; export default StringUtils;

View file

@ -38,64 +38,11 @@
v-for="notification in sortedNotifications" v-for="notification in sortedNotifications"
:key="notification.id" :key="notification.id"
> >
<article <component
class="notification | bg-white rounded-lg | p-16 | flow" :is="notificationComponents[notification.type]"
:data-status="notification.isRead ? 'read' : 'unread'" :notification="notification"
:data-type="notification.type" />
@click="read(notification)"
>
<header>
<p class="flex">
<strong
class="notification__type | font-medium text-primary"
:data-icon="notification.type"
>Nouveau
{{
notification.type === "comment" ? "commentaire" : "contenu"
}}</strong
>
<span class="notification__client | text-grey-700">{{
notification.location.project.title
}}</span>
<time
datetime=""
class="notification__date | text-grey-700 | ml-auto"
>{{ formatDate(notification) }}</time
>
</p>
</header>
<p
v-if="notification.type"
class="notification__body | text-md font-medium | line-clamp"
>
{{ notification.text }}
</p>
</article>
</template> </template>
<!-- <article
class="notification | bg-white rounded-lg | p-16 | flow"
data-status="read"
data-type="content"
>
<header>
<p class="flex">
<strong
class="notification__type | font-medium text-primary"
data-icon="content"
>Nouveau contenu</strong
>
<span class="notification__client | text-grey-700"></span>
<time
datetime=""
class="notification__date | text-grey-700 | ml-auto"
>Hier</time
>
</p>
</header>
<p class="notification__body | text-md font-medium | line-clamp">
Proposition 2
</p>
</article> -->
</section> </section>
</main> </main>
</template> </template>
@ -108,7 +55,9 @@ import { useUserStore } from "../stores/user";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useApiStore } from "../stores/api"; import { useApiStore } from "../stores/api";
import { useRouter } from "vue-router"; import Comment from "../components/notifications/Comment.vue";
import Reply from "../components/notifications/Reply.vue";
import Content from "../components/notifications/Content.vue";
dayjs.locale("fr"); dayjs.locale("fr");
@ -135,7 +84,11 @@ const tabs = computed(() => {
]; ];
}); });
const router = useRouter(); const notificationComponents = {
comment: Comment,
"comment-reply": Reply,
content: Content,
};
const sortedNotifications = computed(() => { const sortedNotifications = computed(() => {
return [...notifications.value].sort((a, b) => { return [...notifications.value].sort((a, b) => {
@ -147,22 +100,6 @@ function changeTab(newValue) {
currentTab.value = newValue; currentTab.value = newValue;
} }
function formatDate(notification) {
const notificationDigitalDate = parseInt(
dayjs(notification.date).format("YYYYMMDD")
);
const todayDigitalDate = parseInt(dayjs().format("YYYYMMDD"));
const isToday = notificationDigitalDate === todayDigitalDate;
if (isToday) {
return dayjs(notification.date).format("HH:mm");
} else if (todayDigitalDate === notificationDigitalDate + 1) {
return "hier";
} else {
return dayjs(notification.date).format("DD MMM YY");
}
}
function readAll() { function readAll() {
try { try {
api.readAllNotifications(); api.readAllNotifications();
@ -171,24 +108,6 @@ function readAll() {
console.log("Could not read all notifications : ", error); console.log("Could not read all notifications : ", error);
} }
} }
function read(notification) {
if (!notification.id) {
console.error("Couldn't change notification status because it has no id.");
} else if (!notification.isRead) {
api
.readNotification(notification.id)
.then((res) => {
router.push(toPath(notification.location.href));
})
.catch((err) => console.error("Notification could not be read.", err));
}
router.push(toPath(notification.location.href));
}
function toPath(string) {
return string.replace(window.location.origin, "");
}
</script> </script>
<style scoped> <style scoped>
@ -207,13 +126,17 @@ main > header [role="tablist"] {
.notifications { .notifications {
--flow-space: var(--space-16); --flow-space: var(--space-16);
} }
.notification { </style>
<!-- WARNING : below styles are not scoped so as to be applied to the children components -->
<style>
.notifications .notification {
--flow-space: var(--space-12); --flow-space: var(--space-12);
font-size: var(--text-sm); font-size: var(--text-sm);
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
.notification[data-status="unread"]::after { .notifications .notification[data-status="unread"]::after {
content: ""; content: "";
display: inline-block; display: inline-block;
width: 0.5rem; width: 0.5rem;
@ -225,22 +148,22 @@ main > header [role="tablist"] {
top: 0; top: 0;
right: 0; right: 0;
} }
.notification p { .notifications .notification p {
max-width: 100%; max-width: 100%;
} }
.notification header p { .notifications .notification header p {
column-gap: 0; column-gap: 0;
} }
.notification__type { .notifications .notification__type {
display: flex; display: flex;
} }
.notification__type::before { .notifications .notification__type::before {
margin-right: var(--space-8); margin-right: var(--space-8);
} }
.notification__client { .notifications .notification__client {
display: inline-block; display: inline-block;
} }
.notification__client::before { .notifications .notification__client::before {
content: ""; content: "";
display: inline-block; display: inline-block;
width: 3px; width: 3px;
@ -249,7 +172,7 @@ main > header [role="tablist"] {
border-radius: 50%; border-radius: 50%;
margin: 0.2em 0.5em; margin: 0.2em 0.5em;
} }
.notification__body { .notifications .notification__body {
--line-clamp: 2; --line-clamp: 2;
} }
</style> </style>