Refresh
This commit is contained in:
15
resources/css/app.css
vendored
Executable file
15
resources/css/app.css
vendored
Executable file
@@ -0,0 +1,15 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
@import "base/_index";
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@import "components/_index";
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@import "utilities/_index";
|
||||
}
|
||||
4
resources/css/base/_index.css
vendored
Executable file
4
resources/css/base/_index.css
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
@import "article.css";
|
||||
@import "forms/_index.css";
|
||||
@import "scrollbar.css";
|
||||
@import "details.css";
|
||||
37
resources/css/base/article.css
vendored
Executable file
37
resources/css/base/article.css
vendored
Executable file
@@ -0,0 +1,37 @@
|
||||
article {
|
||||
@apply h-full w-full flex flex-col;
|
||||
|
||||
& header {
|
||||
@apply flex-none flex flex-row items-center justify-between text-lg p-2;
|
||||
@apply bg-gray-200;
|
||||
@apply dark:bg-gray-600 dark:text-white;
|
||||
|
||||
& .icons,
|
||||
& .badges,
|
||||
& .tools {
|
||||
@apply flex-shrink flex items-center;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
@apply flex-grow truncate;
|
||||
}
|
||||
|
||||
& .badges,
|
||||
& .tools > * {
|
||||
@apply ml-2;
|
||||
}
|
||||
|
||||
& img,
|
||||
& svg,
|
||||
& .caret {
|
||||
@apply mr-1;
|
||||
|
||||
max-width: 1.125rem;
|
||||
max-height: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
& .body {
|
||||
@apply flex-grow overflow-auto p-2 xl:p-4;
|
||||
}
|
||||
}
|
||||
15
resources/css/base/details.css
vendored
Executable file
15
resources/css/base/details.css
vendored
Executable file
@@ -0,0 +1,15 @@
|
||||
details {
|
||||
&.details-block {
|
||||
@apply mb-1 rounded w-full;
|
||||
@apply bg-gray-100;
|
||||
@apply dark:bg-gray-700;
|
||||
}
|
||||
|
||||
& summary {
|
||||
@apply px-2 py-1 cursor-pointer;
|
||||
}
|
||||
|
||||
& .body {
|
||||
@apply p-2;
|
||||
}
|
||||
}
|
||||
8
resources/css/base/forms/_index.css
vendored
Executable file
8
resources/css/base/forms/_index.css
vendored
Executable file
@@ -0,0 +1,8 @@
|
||||
@import "form.css";
|
||||
@import "form-group.css";
|
||||
@import "input-group.css";
|
||||
@import "label.css";
|
||||
@import "input.css";
|
||||
@import "button.css";
|
||||
@import "textarea.css";
|
||||
@import "select.css";
|
||||
53
resources/css/base/forms/button.css
vendored
Executable file
53
resources/css/base/forms/button.css
vendored
Executable file
@@ -0,0 +1,53 @@
|
||||
button,
|
||||
a.button {
|
||||
@apply rounded text-left flex-shrink flex items-center whitespace-nowrap space-x-1;
|
||||
|
||||
& svg {
|
||||
@apply flex-none;
|
||||
}
|
||||
|
||||
&[type="submit"],
|
||||
&.info {
|
||||
@apply px-2 py-1 text-sm;
|
||||
@apply bg-blue-500 text-white;
|
||||
@apply dark:bg-blue-800 dark:text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-blue-400;
|
||||
@apply dark:bg-blue-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply px-2 py-1 text-sm;
|
||||
@apply bg-green-500 text-white;
|
||||
@apply dark:bg-green-800 dark:text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-green-400;
|
||||
@apply dark:bg-green-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@apply px-2 py-1 text-sm;
|
||||
@apply bg-red-500 text-white;
|
||||
@apply dark:bg-red-800 dark:text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-red-400;
|
||||
@apply dark:bg-red-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
@apply px-2 py-1 text-sm;
|
||||
@apply bg-gray-400 text-white;
|
||||
@apply dark:bg-gray-700 dark:text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-300;
|
||||
@apply dark:bg-gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
resources/css/base/forms/form-group.css
vendored
Executable file
7
resources/css/base/forms/form-group.css
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
.form-group {
|
||||
@apply mb-4 w-full;
|
||||
|
||||
&.last {
|
||||
@apply mt-8 mb-0;
|
||||
}
|
||||
}
|
||||
3
resources/css/base/forms/form.css
vendored
Executable file
3
resources/css/base/forms/form.css
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
form {
|
||||
@apply mb-2;
|
||||
}
|
||||
3
resources/css/base/forms/input-group.css
vendored
Executable file
3
resources/css/base/forms/input-group.css
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
.input-group {
|
||||
@apply flex items-stretch space-x-2 w-full;
|
||||
}
|
||||
24
resources/css/base/forms/input.css
vendored
Executable file
24
resources/css/base/forms/input.css
vendored
Executable file
@@ -0,0 +1,24 @@
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
input[type="url"] {
|
||||
@apply w-full flex-grow rounded px-2 py-1 border;
|
||||
@apply bg-white text-gray-500 border-gray-300;
|
||||
@apply dark:bg-gray-600 dark:text-gray-100 dark:border-gray-600;
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
@apply w-full flex-grow rounded px-2 py-1 border;
|
||||
@apply bg-white text-gray-500 border-gray-300;
|
||||
@apply dark:bg-gray-500 dark:text-gray-100 dark:border-gray-600;
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
@apply tracking-widest;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
@apply rounded p-0 m-0 h-8 w-12 border-none appearance-none;
|
||||
@apply bg-white;
|
||||
@apply dark:bg-gray-600;
|
||||
}
|
||||
3
resources/css/base/forms/label.css
vendored
Executable file
3
resources/css/base/forms/label.css
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
label {
|
||||
@apply mb-1 flex-shrink whitespace-nowrap flex items-center;
|
||||
}
|
||||
5
resources/css/base/forms/select.css
vendored
Executable file
5
resources/css/base/forms/select.css
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
select {
|
||||
@apply mt-1 block w-full py-0 px-2 border rounded focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm;
|
||||
@apply border-gray-300 bg-white;
|
||||
@apply dark:border-gray-500 dark:bg-gray-500 dark:text-white;
|
||||
}
|
||||
5
resources/css/base/forms/textarea.css
vendored
Executable file
5
resources/css/base/forms/textarea.css
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
textarea {
|
||||
@apply rounded w-full;
|
||||
@apply bg-white;
|
||||
@apply dark:bg-gray-500 dark:text-white;
|
||||
}
|
||||
24
resources/css/base/scrollbar.css
vendored
Executable file
24
resources/css/base/scrollbar.css
vendored
Executable file
@@ -0,0 +1,24 @@
|
||||
/* The emerging W3C standard
|
||||
that is currently Firefox-only */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: theme("colors.white") theme("colors.gray.300");
|
||||
|
||||
&:dark {
|
||||
scrollbar-color: theme("colors.gray.900") theme("colors.gray.800");
|
||||
}
|
||||
}
|
||||
|
||||
/* Works on Chrome/Edge/Safari */
|
||||
*::-webkit-scrollbar {
|
||||
@apply w-2;
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
@apply bg-gray-300;
|
||||
@apply dark:bg-gray-900;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
@apply rounded;
|
||||
@apply bg-white;
|
||||
@apply dark:bg-gray-500;
|
||||
}
|
||||
6
resources/css/components/_index.css
vendored
Executable file
6
resources/css/components/_index.css
vendored
Executable file
@@ -0,0 +1,6 @@
|
||||
@import "list.css";
|
||||
@import "badge.css";
|
||||
@import "folder.css";
|
||||
@import "highlight.css";
|
||||
@import "prose.css";
|
||||
@import "group-status.css";
|
||||
27
resources/css/components/badge.css
vendored
Executable file
27
resources/css/components/badge.css
vendored
Executable file
@@ -0,0 +1,27 @@
|
||||
.badge {
|
||||
@apply rounded-full text-xs flex items-center whitespace-nowrap px-2 py-0.5 space-x-1;
|
||||
|
||||
&.default {
|
||||
@apply bg-gray-100 text-gray-500;
|
||||
@apply dark:bg-gray-500 dark:text-white;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply bg-green-500 text-white;
|
||||
@apply dark:bg-green-800 dark:text-white;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@apply bg-red-500 text-white;
|
||||
@apply dark:bg-red-800 dark:text-white;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply bg-orange-500 text-white;
|
||||
}
|
||||
|
||||
&.info {
|
||||
@apply bg-blue-400 text-white;
|
||||
@apply dark:bg-blue-800 dark:text-white;
|
||||
}
|
||||
}
|
||||
19
resources/css/components/folder.css
vendored
Executable file
19
resources/css/components/folder.css
vendored
Executable file
@@ -0,0 +1,19 @@
|
||||
/* Regular folder */
|
||||
.folder-common {
|
||||
@apply text-orange-400;
|
||||
}
|
||||
|
||||
/* Unread items folder - with unread items */
|
||||
.folder-unread-not-empty {
|
||||
@apply text-purple-400;
|
||||
}
|
||||
|
||||
/* Unread items folder - empty */
|
||||
.folder-unread {
|
||||
@apply text-gray-500;
|
||||
}
|
||||
|
||||
/* Root folder */
|
||||
.folder-root {
|
||||
@apply text-blue-400;
|
||||
}
|
||||
26
resources/css/components/group-status.css
vendored
Executable file
26
resources/css/components/group-status.css
vendored
Executable file
@@ -0,0 +1,26 @@
|
||||
.group-status-own {
|
||||
@apply text-white bg-green-800;
|
||||
}
|
||||
|
||||
.group-status-created {
|
||||
@apply text-white bg-green-800;
|
||||
}
|
||||
|
||||
.group-status-invited {
|
||||
@apply text-white bg-orange-700;
|
||||
}
|
||||
|
||||
.group-status-accepted {
|
||||
@apply text-white bg-green-800;
|
||||
}
|
||||
|
||||
.group-status-rejected {
|
||||
@apply text-white bg-red-800;
|
||||
}
|
||||
|
||||
.group-status-left {
|
||||
}
|
||||
|
||||
.group-status-joining {
|
||||
@apply text-white bg-orange-700;
|
||||
}
|
||||
3
resources/css/components/highlight.css
vendored
Executable file
3
resources/css/components/highlight.css
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
.highlight {
|
||||
@apply rounded px-1;
|
||||
}
|
||||
160
resources/css/components/list.css
vendored
Executable file
160
resources/css/components/list.css
vendored
Executable file
@@ -0,0 +1,160 @@
|
||||
.list {
|
||||
&.vertical {
|
||||
@apply flex flex-col p-1;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
@apply flex flex-row flex-wrap p-0;
|
||||
}
|
||||
|
||||
&.items-rounded > * {
|
||||
@apply rounded;
|
||||
}
|
||||
|
||||
&.spaced > * {
|
||||
@apply mb-0.5;
|
||||
}
|
||||
|
||||
&.compact > * {
|
||||
@apply py-1;
|
||||
}
|
||||
|
||||
&.alt {
|
||||
@apply rounded;
|
||||
@apply bg-gray-50;
|
||||
@apply dark:bg-gray-850;
|
||||
}
|
||||
|
||||
& > * {
|
||||
@apply px-2 py-2 flex-none;
|
||||
|
||||
&.emphasize {
|
||||
@apply text-blue-300;
|
||||
@apply dark:text-gray-100;
|
||||
|
||||
&:hover {
|
||||
@apply text-blue-500;
|
||||
@apply dark:text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.list-item,
|
||||
& .list-item {
|
||||
@apply flex items-center justify-between space-x-1;
|
||||
|
||||
& .list-item-text {
|
||||
@apply flex-grow truncate;
|
||||
}
|
||||
|
||||
& .list-item-title {
|
||||
@apply mr-2 whitespace-nowrap;
|
||||
}
|
||||
|
||||
& .list-item-value {
|
||||
@apply text-right;
|
||||
|
||||
& code {
|
||||
@apply break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .icons,
|
||||
& .badges,
|
||||
& .handle {
|
||||
@apply flex-none flex items-center space-x-1;
|
||||
}
|
||||
|
||||
& .handle {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
& .caret {
|
||||
@apply dark:text-gray-500;
|
||||
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
|
||||
&.text-xs {
|
||||
& img,
|
||||
& svg,
|
||||
& .caret {
|
||||
max-width: 0.75rem;
|
||||
max-height: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
& img,
|
||||
& svg,
|
||||
& .caret {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
& img.favicon-lg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
@apply flex-none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-200 text-gray-500;
|
||||
@apply dark:bg-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply bg-blue-500 text-white;
|
||||
@apply dark:bg-blue-800 dark:text-white;
|
||||
|
||||
& .caret {
|
||||
@apply dark:text-gray-500;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-blue-400;
|
||||
@apply dark:bg-blue-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.dragged-over {
|
||||
@apply bg-green-200 text-gray-500;
|
||||
@apply dark:bg-green-800 dark:text-white;
|
||||
}
|
||||
|
||||
&.cannot-drop {
|
||||
@apply bg-red-200 text-gray-500;
|
||||
@apply dark:bg-red-800 dark:text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.striped > * {
|
||||
@apply odd:bg-white;
|
||||
@apply dark:odd:bg-gray-750;
|
||||
|
||||
&.emphasize {
|
||||
&:hover {
|
||||
@apply text-blue-500;
|
||||
@apply dark:text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-200 text-gray-500;
|
||||
@apply dark:bg-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply bg-blue-500 text-white;
|
||||
@apply dark:bg-blue-800 dark:text-white;
|
||||
|
||||
& .caret {
|
||||
@apply dark:text-gray-500;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-blue-400 text-white;
|
||||
@apply dark:bg-blue-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
resources/css/components/prose.css
vendored
Executable file
109
resources/css/components/prose.css
vendored
Executable file
@@ -0,0 +1,109 @@
|
||||
.cyca-prose {
|
||||
@apply mx-auto mb-6 max-w-prose shadow-lg rounded px-2 py-2 text-justify;
|
||||
@apply bg-white;
|
||||
@apply dark:bg-gray-700;
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
& p {
|
||||
@apply text-justify my-4 leading-7;
|
||||
}
|
||||
|
||||
& img {
|
||||
@apply my-2;
|
||||
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
& figure {
|
||||
& figcaption {
|
||||
@apply text-center italic text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
& pre {
|
||||
@apply bg-gray-900 text-gray-400 p-2 rounded overflow-auto;
|
||||
}
|
||||
|
||||
& table {
|
||||
@apply min-w-full divide-y border;
|
||||
@apply divide-gray-300 border-gray-300;
|
||||
@apply dark:divide-gray-800 dark:border-gray-800;
|
||||
|
||||
& tbody {
|
||||
@apply divide-y;
|
||||
@apply divide-gray-300;
|
||||
@apply dark:divide-gray-800;
|
||||
|
||||
& tr {
|
||||
@apply odd:bg-gray-100;
|
||||
@apply dark:odd:bg-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
& td,
|
||||
& th {
|
||||
@apply px-2 py-1;
|
||||
}
|
||||
}
|
||||
|
||||
& h1,
|
||||
& h2,
|
||||
& h3,
|
||||
& h4,
|
||||
& h5,
|
||||
& h6 {
|
||||
@apply text-black;
|
||||
@apply dark:text-white;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
@apply text-2xl my-6;
|
||||
}
|
||||
|
||||
& h2 {
|
||||
@apply text-xl my-4;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
@apply text-lg my-2;
|
||||
}
|
||||
|
||||
& ul,
|
||||
& ol {
|
||||
@apply mx-8;
|
||||
|
||||
& li {
|
||||
@apply my-2 p-2 rounded;
|
||||
|
||||
@apply odd:bg-gray-100;
|
||||
@apply dark:odd:bg-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
& ul {
|
||||
@apply list-disc;
|
||||
}
|
||||
|
||||
& ol {
|
||||
@apply list-decimal;
|
||||
}
|
||||
|
||||
& a {
|
||||
@apply underline;
|
||||
@apply text-blue-300;
|
||||
@apply dark:text-blue-500;
|
||||
|
||||
&:hover {
|
||||
@apply no-underline;
|
||||
@apply text-blue-400;
|
||||
@apply dark:text-blue-400;
|
||||
}
|
||||
}
|
||||
|
||||
& blockquote {
|
||||
@apply border-l-4 pl-4 italic mx-8 rounded p-2;
|
||||
@apply border-gray-500 bg-gray-100;
|
||||
@apply dark:border-gray-600 dark:bg-gray-800;
|
||||
}
|
||||
}
|
||||
2
resources/css/utilities/_index.css
vendored
Executable file
2
resources/css/utilities/_index.css
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
@import "readable.css";
|
||||
@import "http-status.css";
|
||||
23
resources/css/utilities/http-status.css
vendored
Executable file
23
resources/css/utilities/http-status.css
vendored
Executable file
@@ -0,0 +1,23 @@
|
||||
.http-status-general-error {
|
||||
@apply text-red-500;
|
||||
}
|
||||
|
||||
.http-status-info {
|
||||
|
||||
}
|
||||
|
||||
.http-status-success {
|
||||
@apply text-green-500;
|
||||
}
|
||||
|
||||
.http-status-redirection {
|
||||
@apply text-blue-500;
|
||||
}
|
||||
|
||||
.http-status-client-error {
|
||||
@apply text-orange-500;
|
||||
}
|
||||
|
||||
.http-status-server-error {
|
||||
@apply text-red-500;
|
||||
}
|
||||
32
resources/css/utilities/readable.css
vendored
Executable file
32
resources/css/utilities/readable.css
vendored
Executable file
@@ -0,0 +1,32 @@
|
||||
.readable {
|
||||
@apply font-serif text-xl no-underline break-all;
|
||||
letter-spacing: 0.2rem;
|
||||
|
||||
& .letter {
|
||||
}
|
||||
|
||||
& .capital {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
& .number {
|
||||
@apply text-green-400;
|
||||
}
|
||||
|
||||
& .other {
|
||||
@apply text-purple-500 font-bold;
|
||||
}
|
||||
|
||||
& .empty::before {
|
||||
@apply text-blue-400 inline-block;
|
||||
content: "❡";
|
||||
}
|
||||
|
||||
& .operator {
|
||||
@apply text-purple-500 font-bold;
|
||||
}
|
||||
|
||||
& .suspicious {
|
||||
@apply text-red-500 font-bold;
|
||||
}
|
||||
}
|
||||
1
resources/examples/cron
Executable file
1
resources/examples/cron
Executable file
@@ -0,0 +1 @@
|
||||
* * * * * /usr/local/bin/php /var/www/artisan schedule:run > /proc/1/fd/1 2>/proc/1/fd/2
|
||||
32
resources/examples/nginx.conf
Executable file
32
resources/examples/nginx.conf
Executable file
@@ -0,0 +1,32 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /var/www/public;
|
||||
index index.html index.htm index.php;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
access_log off;
|
||||
error_log stderr error;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location /app {
|
||||
proxy_pass http://localhost:6001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
10
resources/examples/supervisor/cyca_queue.conf
Executable file
10
resources/examples/supervisor/cyca_queue.conf
Executable file
@@ -0,0 +1,10 @@
|
||||
[program:cyca_queue]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php /var/www/cyca/artisan queue:work --queue=notifications,default
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=www-data
|
||||
numprocs=8
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/www/cyca/storage/logs/queues.log
|
||||
stopwaitsecs=3600
|
||||
10
resources/examples/supervisor/cyca_websocket.conf
Executable file
10
resources/examples/supervisor/cyca_websocket.conf
Executable file
@@ -0,0 +1,10 @@
|
||||
[program:cyca_websocket]
|
||||
process_name=%(program_name)s
|
||||
command=php artisan websockets:serve
|
||||
directory=/var/www/cyca
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=www-data
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/www/cyca/storage/logs/echo-server.log
|
||||
stopwaitsecs=3600
|
||||
44
resources/initial_data.json
Executable file
44
resources/initial_data.json
Executable file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"highlights": [{ "expression": "cyca", "color": "#1e40af" }],
|
||||
"bookmarks": {
|
||||
"documents": [],
|
||||
"folders": [
|
||||
{
|
||||
"title": "Cyca",
|
||||
"documents": [
|
||||
{
|
||||
"url": "https://www.getcyca.com/",
|
||||
"feeds": [
|
||||
{
|
||||
"url": "https://www.getcyca.com/index.xml",
|
||||
"is_ignored": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://alternativeto.net/software/cyca/",
|
||||
"feeds": [
|
||||
{
|
||||
"url": "https://alternativeto.net/news/feed/",
|
||||
"is_ignored": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/RichardDern/Cyca",
|
||||
"feeds": [
|
||||
{
|
||||
"url": "https://github.com/RichardDern/Cyca/commits/master.atom",
|
||||
"is_ignored": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://microblog.getcyca.com/"
|
||||
}
|
||||
],
|
||||
"folders": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
250
resources/js/app.js
vendored
Executable file
250
resources/js/app.js
vendored
Executable file
@@ -0,0 +1,250 @@
|
||||
require("./modules/bootstrap");
|
||||
require("./modules/websockets");
|
||||
|
||||
import { createApp } from "vue";
|
||||
import mixins from "./mixins";
|
||||
import store from "./store";
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
|
||||
import DetailsDocument from "./components/Details/DetailsDocument.vue";
|
||||
import DetailsDocuments from "./components/Details/DetailsDocuments.vue";
|
||||
import DetailsFeedItem from "./components/Details/DetailsFeedItem.vue";
|
||||
import DetailsFolder from "./components/Details/DetailsFolder.vue";
|
||||
import DocumentsList from "./components/DocumentsList.vue";
|
||||
import FeedItemsList from "./components/FeedItemsList.vue";
|
||||
import FoldersTree from "./components/FoldersTree.vue";
|
||||
import { collect } from "collect.js";
|
||||
|
||||
createApp({
|
||||
components: {
|
||||
DetailsDocument,
|
||||
DetailsDocuments,
|
||||
DetailsFeedItem,
|
||||
DetailsFolder,
|
||||
DocumentsList,
|
||||
FeedItemsList,
|
||||
FoldersTree,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
detailsViewComponent: null,
|
||||
showFeedItemsList: false,
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
const self = this;
|
||||
|
||||
self.listenToBroadcast();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedFolder: "folders/selectedFolder",
|
||||
documents: "documents/documents",
|
||||
selectedDocuments: "documents/selectedDocuments",
|
||||
feedItems: "feedItems/feedItems",
|
||||
selectedFeedItems: "feedItems/selectedFeedItems",
|
||||
getUnreadItemsFolder: "folders/getUnreadItemsFolder",
|
||||
selectedGroup: "groups/selectedGroup",
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
selectedFolder: function (oldFolder, newFolder) {
|
||||
const self = this;
|
||||
|
||||
if (newFolder) {
|
||||
self.detailsViewComponent = "details-folder";
|
||||
}
|
||||
},
|
||||
selectedDocuments: function (documents) {
|
||||
const self = this;
|
||||
|
||||
if (documents && documents.length > 0) {
|
||||
if (documents.length === 1) {
|
||||
self.detailsViewComponent = "details-document";
|
||||
} else {
|
||||
self.detailsViewComponent = "details-documents";
|
||||
}
|
||||
} else {
|
||||
self.detailsViewComponent = "details-folder";
|
||||
}
|
||||
},
|
||||
selectedFeedItems: function (feedItems) {
|
||||
const self = this;
|
||||
|
||||
if (feedItems && feedItems.length > 0) {
|
||||
if (feedItems.length === 1) {
|
||||
self.detailsViewComponent = "details-feed-item";
|
||||
} else {
|
||||
//TODO: Handle multiple selected feed items ?
|
||||
self.detailsViewComponent = null;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
self.selectedDocuments &&
|
||||
self.selectedDocuments.length > 0
|
||||
) {
|
||||
self.detailsViewComponent = "details-document";
|
||||
} else {
|
||||
self.detailsViewComponent = "details-folder";
|
||||
}
|
||||
}
|
||||
},
|
||||
feedItems: function (value) {
|
||||
this.showFeedItemsList = value && collect(value).count() > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
showFolder: "folders/show",
|
||||
indexFolders: "folders/index",
|
||||
selectDocuments: "documents/selectDocuments",
|
||||
indexDocuments: "documents/index",
|
||||
dropIntoFolder: "folders/dropIntoFolder",
|
||||
selectFeedItems: "feedItems/selectFeedItems",
|
||||
markFeedItemsAsRead: "feedItems/markAsRead",
|
||||
updateDocument: "documents/update",
|
||||
deleteDocuments: "documents/destroy",
|
||||
updateFolder: "folders/updateProperties",
|
||||
showGroup: "groups/show",
|
||||
updateGroup: "groups/updateProperties",
|
||||
resetUnreadCount: "folders/resetUnreadCount",
|
||||
updateUnreadFeedItemsCount: "feedItems/updateUnreadFeedItemsCount",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Listen to broadcast events
|
||||
*/
|
||||
listenToBroadcast: function () {
|
||||
const self = this;
|
||||
const userId = document
|
||||
.querySelector('meta[name="user-id"]')
|
||||
.getAttribute("content");
|
||||
|
||||
window.Echo.private("App.Models.User." + userId).notification(
|
||||
(notification) => {
|
||||
switch (notification.type) {
|
||||
case "App\\Notifications\\UnreadItemsChanged":
|
||||
self.updateUnreadFeedItemsCount(notification);
|
||||
self.showFolder({
|
||||
deselectDocuments: false,
|
||||
updateFeedItems: true,
|
||||
});
|
||||
break;
|
||||
case "App\\Notifications\\DocumentUpdated":
|
||||
self.updateDocument({
|
||||
documentId: notification.document.id,
|
||||
newProperties: notification.document,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* User-action - Selected group has changed
|
||||
* @param {*} group
|
||||
*/
|
||||
onSelectedGroupChanged: function (group) {
|
||||
const self = this;
|
||||
|
||||
self.showGroup({ group: group, folder: "unread_items" });
|
||||
},
|
||||
|
||||
/**
|
||||
* Groups have been loaded
|
||||
*/
|
||||
onGroupsLoaded: function () {
|
||||
//
|
||||
},
|
||||
|
||||
/**
|
||||
* Folders tree has been loaded
|
||||
*/
|
||||
onFoldersLoaded: function () {
|
||||
//
|
||||
},
|
||||
|
||||
/**
|
||||
* User-action - Selected folder has changed
|
||||
* @param {*} folder
|
||||
*/
|
||||
onSelectedFolderChanged: function (folder, group) {
|
||||
const self = this;
|
||||
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (group && group != self.selectedGroup.id) {
|
||||
self.showGroup({ group, folder });
|
||||
} else {
|
||||
self.showFolder({ folder });
|
||||
}
|
||||
|
||||
self.detailsViewComponent = "details-folder";
|
||||
},
|
||||
|
||||
/**
|
||||
* User-action - Something has been dropped into a folder
|
||||
*/
|
||||
onItemDropped: function (folder) {
|
||||
const self = this;
|
||||
|
||||
self.dropIntoFolder(folder);
|
||||
},
|
||||
|
||||
/**
|
||||
* User-action - Selected documents has changed
|
||||
*/
|
||||
onSelectedDocumentsChanged: function (documents) {
|
||||
const self = this;
|
||||
|
||||
self.selectDocuments({ documents: documents });
|
||||
|
||||
if (self.feedItems.length > 0) {
|
||||
document.getElementById("feed-items-list").scrollTop = 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* User-action - Refresh documents list after adding one
|
||||
*/
|
||||
onDocumentAdded: function () {
|
||||
//
|
||||
},
|
||||
|
||||
/**
|
||||
* User-action - Refresh documents list after deleting one (or more)
|
||||
*/
|
||||
onDocumentsDeleted: function ({ folder, documents }) {
|
||||
const self = this;
|
||||
|
||||
self.deleteDocuments({
|
||||
documents: documents,
|
||||
folder: folder,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* User-action - Selected feed items has changed
|
||||
*/
|
||||
onSelectedFeedItemsChanged: function (feedItems) {
|
||||
const self = this;
|
||||
|
||||
self.selectFeedItems(feedItems);
|
||||
},
|
||||
|
||||
/**
|
||||
* User-action - Feed items marked as read
|
||||
*/
|
||||
onFeedItemsRead: function (data) {
|
||||
const self = this;
|
||||
|
||||
self.markFeedItemsAsRead(data);
|
||||
},
|
||||
},
|
||||
})
|
||||
.mixin(mixins)
|
||||
.use(store)
|
||||
.mount("#app");
|
||||
28
resources/js/bootstrap.js
vendored
Executable file
28
resources/js/bootstrap.js
vendored
Executable file
@@ -0,0 +1,28 @@
|
||||
window._ = require('lodash');
|
||||
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
* to our Laravel back-end. This library automatically handles sending the
|
||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||
*/
|
||||
|
||||
window.axios = require('axios');
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
/**
|
||||
* Echo exposes an expressive API for subscribing to channels and listening
|
||||
* for events that are broadcast by Laravel. Echo and event broadcasting
|
||||
* allows your team to easily build robust real-time web applications.
|
||||
*/
|
||||
|
||||
// import Echo from 'laravel-echo';
|
||||
|
||||
// window.Pusher = require('pusher-js');
|
||||
|
||||
// window.Echo = new Echo({
|
||||
// broadcaster: 'pusher',
|
||||
// key: process.env.MIX_PUSHER_APP_KEY,
|
||||
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
||||
// forceTLS: true
|
||||
// });
|
||||
40
resources/js/components/DateTime.vue
Executable file
40
resources/js/components/DateTime.vue
Executable file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<time v-bind:datetime="iso">
|
||||
{{ formatted }}
|
||||
</time>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["datetime", "onlyDate", "onlyTime"],
|
||||
computed: {
|
||||
iso: function () {
|
||||
if (!this.datetime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(this.datetime).toISOString();
|
||||
},
|
||||
formatted: function () {
|
||||
const date = new Date(this.datetime);
|
||||
|
||||
if (this.onlyTime) {
|
||||
return date.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} else if (this.onlyDate) {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
return (
|
||||
date.toLocaleDateString() +
|
||||
", " +
|
||||
date.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
57
resources/js/components/Details/DefaultFolderPermissions.vue
Executable file
57
resources/js/components/Details/DefaultFolderPermissions.vue
Executable file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<stateful-details name="default_folder_permissions_details">
|
||||
<summary>{{ __("Users without explicit permissions can") }}:</summary>
|
||||
|
||||
<div class="body flex items-center mt-2 space-x-2">
|
||||
<permission-box
|
||||
v-bind:text="__('Create folder')"
|
||||
ability="can_create_folder"
|
||||
v-bind:folder="folder"
|
||||
></permission-box>
|
||||
<permission-box
|
||||
v-bind:text="__('Update folder')"
|
||||
ability="can_update_folder"
|
||||
v-bind:folder="folder"
|
||||
></permission-box>
|
||||
<permission-box
|
||||
v-bind:text="__('Delete folder')"
|
||||
ability="can_delete_folder"
|
||||
v-bind:folder="folder"
|
||||
></permission-box>
|
||||
<permission-box
|
||||
v-bind:text="__('Create document')"
|
||||
ability="can_create_document"
|
||||
v-bind:folder="folder"
|
||||
></permission-box>
|
||||
<permission-box
|
||||
v-bind:text="__('Delete document')"
|
||||
ability="can_delete_document"
|
||||
v-bind:folder="folder"
|
||||
></permission-box>
|
||||
</div>
|
||||
</stateful-details>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PermissionBox from "./PermissionBox.vue";
|
||||
import StatefulDetails from "../StatefulDetails.vue";
|
||||
|
||||
export default {
|
||||
components: { PermissionBox, StatefulDetails },
|
||||
props: ["folder"],
|
||||
methods: {
|
||||
can: function (permission) {
|
||||
const self = this;
|
||||
|
||||
if (
|
||||
"user_permissions" in self.folder &&
|
||||
permission in self.folder.user_permissions
|
||||
) {
|
||||
return self.folder.user_permissions[permission];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
500
resources/js/components/Details/DetailsDocument.vue
Executable file
500
resources/js/components/Details/DetailsDocument.vue
Executable file
@@ -0,0 +1,500 @@
|
||||
<template>
|
||||
<article v-if="document">
|
||||
<header>
|
||||
<div class="icons">
|
||||
<img v-bind:src="document.favicon" />
|
||||
</div>
|
||||
|
||||
<h1>{{ document.title }}</h1>
|
||||
|
||||
<div class="badges">
|
||||
<div
|
||||
class="badge default"
|
||||
v-if="document.feed_item_states_count > 0"
|
||||
>
|
||||
{{ document.feed_item_states_count }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tools">
|
||||
<button
|
||||
v-if="document.feed_item_states_count > 0"
|
||||
class="info"
|
||||
v-on:click="onMarkAsReadClicked"
|
||||
v-bind:title="__('Mark as read')"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('unread_items')" />
|
||||
</svg>
|
||||
<span class="hidden xl:inline-block">
|
||||
{{ __("Mark as read") }}
|
||||
</span>
|
||||
</button>
|
||||
<a
|
||||
class="button info"
|
||||
v-bind:href="url"
|
||||
rel="noopener noreferrer"
|
||||
v-on:click.left.stop.prevent="
|
||||
openDocument({
|
||||
document: document,
|
||||
})
|
||||
"
|
||||
v-on:click.middle.exact="
|
||||
incrementVisits({
|
||||
document: document,
|
||||
})
|
||||
"
|
||||
v-bind:title="__('Open')"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('open')" />
|
||||
</svg>
|
||||
<span class="hidden xl:inline-block">
|
||||
{{ __("Open") }}
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
class="info"
|
||||
v-on:click="onShareClicked"
|
||||
v-bind:title="__('Share')"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('share')" />
|
||||
</svg>
|
||||
<span class="hidden xl:inline-block">
|
||||
{{ __("Share") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="body">
|
||||
<div
|
||||
class="cyca-prose"
|
||||
v-if="document.description"
|
||||
v-html="document.description"
|
||||
></div>
|
||||
|
||||
<stateful-details name="document_details" class="details-block">
|
||||
<summary>{{ __("Details") }}</summary>
|
||||
<div class="vertical list striped items-rounded compact">
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">{{ __("Real URL") }}</div>
|
||||
<div class="list-item-value">
|
||||
<a
|
||||
v-bind:href="document.url"
|
||||
rel="noopener noreferrer"
|
||||
v-on:click.left.stop.prevent="
|
||||
openDocument({
|
||||
document: document,
|
||||
})
|
||||
"
|
||||
class="readable"
|
||||
v-html="document.ascii_url"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-item" v-if="document.visits">
|
||||
<div class="list-item-title">{{ __("Visits") }}</div>
|
||||
<div class="list-item-value">
|
||||
{{ document.visits }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">
|
||||
{{ __("Date of document's last check") }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
<date-time
|
||||
v-bind:datetime="document.checked_at"
|
||||
v-bind:calendar="true"
|
||||
></date-time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="list-item"
|
||||
v-if="dupplicateInFolders.length > 0"
|
||||
>
|
||||
<div class="list-item-title">
|
||||
{{ __("Also exists in") }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
<div
|
||||
v-for="dupplicateInFolder in dupplicateInFolders"
|
||||
v-bind:key="dupplicateInFolder.id"
|
||||
v-on:click="
|
||||
$emit(
|
||||
'folder-selected',
|
||||
dupplicateInFolder.id,
|
||||
dupplicateInFolder.group_id
|
||||
)
|
||||
"
|
||||
v-html="dupplicateInFolder.breadcrumbs"
|
||||
class="cursor-pointer mb-1"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</stateful-details>
|
||||
|
||||
<stateful-details
|
||||
name="feeds_details"
|
||||
class="details-block"
|
||||
v-if="document.feeds.length > 0"
|
||||
>
|
||||
<summary>{{ __("Feeds") }}</summary>
|
||||
|
||||
<div class="list vertical striped items-rounded">
|
||||
<div v-for="feed in document.feeds" v-bind:key="feed.id">
|
||||
<div class="list-item">
|
||||
<div class="icons">
|
||||
<img v-bind:src="feed.favicon" />
|
||||
</div>
|
||||
<div class="list-item-text">{{ feed.title }}</div>
|
||||
<div class="badges">
|
||||
<button
|
||||
class="success"
|
||||
v-if="feed.is_ignored"
|
||||
v-on:click="follow(feed)"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<use v-bind:xlink:href="icon('join')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Follow") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="danger"
|
||||
v-if="!feed.is_ignored"
|
||||
v-on:click="ignore(feed)"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<use
|
||||
v-bind:xlink:href="icon('cancel')"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Ignore") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="vertical list striped items-rounded compact mt-2 alt"
|
||||
>
|
||||
<div class="list-item" v-if="feed.description">
|
||||
<div v-html="feed.description"></div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">
|
||||
{{ __("Real URL") }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
<div
|
||||
class="readable"
|
||||
v-html="feed.ascii_url"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">
|
||||
{{ __("Date of document's last check") }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
<date-time
|
||||
v-bind:datetime="feed.checked_at"
|
||||
v-bind:calendar="true"
|
||||
></date-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item" v-if="feed.error">
|
||||
<div class="list-item-title">
|
||||
{{ __("Error") }}
|
||||
</div>
|
||||
<div class="list-item-value text-red-500">
|
||||
{{ feed.error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</stateful-details>
|
||||
|
||||
<stateful-details
|
||||
class="details-block"
|
||||
name="document_meta_data_details"
|
||||
v-if="document.meta_data"
|
||||
>
|
||||
<summary>{{ __("Metadata") }}</summary>
|
||||
<meta-data-browser
|
||||
class="vertical list striped items-rounded compact"
|
||||
v-bind:meta-data="document.meta_data"
|
||||
></meta-data-browser>
|
||||
</stateful-details>
|
||||
|
||||
<stateful-details
|
||||
name="http_response_details"
|
||||
class="details-block"
|
||||
>
|
||||
<summary class="list-item">{{ __("HTTP response") }}</summary>
|
||||
<div class="vertical list striped items-rounded compact">
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">
|
||||
{{ __("HTTP Status Code") }}
|
||||
</div>
|
||||
<div
|
||||
class="list-item-value flex items-center space-x-2"
|
||||
v-bind:class="statusClass"
|
||||
>
|
||||
<span v-if="statusIcon">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon(statusIcon)" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>{{ document.http_status_code }}</span>
|
||||
<span>{{ document.http_status_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-item" v-if="document.mimetype">
|
||||
<div class="list-item-title">
|
||||
{{ __("MIME type") }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
{{ document.mimetype }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<stateful-details
|
||||
name="document_response_details"
|
||||
class="list-item"
|
||||
>
|
||||
<summary>
|
||||
{{ __("Response details") }}
|
||||
</summary>
|
||||
<meta-data-browser
|
||||
class="vertical list striped items-rounded compact alt"
|
||||
v-bind:meta-data="document.response"
|
||||
parent-name="document_response"
|
||||
></meta-data-browser>
|
||||
</stateful-details>
|
||||
</div>
|
||||
</stateful-details>
|
||||
|
||||
<div
|
||||
class="mt-6"
|
||||
v-if="
|
||||
selectedFolder.type !== 'unread_items' &&
|
||||
selectedFolder.user_permissions.can_delete_document
|
||||
"
|
||||
>
|
||||
<button class="danger" v-on:click="onDeleteDocument">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('trash')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Delete") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
import DateTime from "../DateTime";
|
||||
import StatefulDetails from "../StatefulDetails.vue";
|
||||
import MetaDataBrowser from "../MetaDataBrowser.vue";
|
||||
|
||||
export default {
|
||||
components: { DateTime, StatefulDetails, MetaDataBrowser },
|
||||
data: function () {
|
||||
return {
|
||||
dupplicateInFolders: [],
|
||||
localStorage: localStorage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
document: "documents/selectedDocument",
|
||||
selectedFolder: "folders/selectedFolder",
|
||||
feeds: "feeds/feeds",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Return document's initial URL instead of the real URL, unless there
|
||||
* is not attached bookmark
|
||||
*/
|
||||
url: function () {
|
||||
const self = this;
|
||||
|
||||
return self.document.url;
|
||||
},
|
||||
|
||||
statusClass: function () {
|
||||
const code = this.document.http_status_code;
|
||||
let className = null;
|
||||
|
||||
if (code === 0) {
|
||||
className = "http-status-general-error";
|
||||
} else if (code >= 100 && code <= 199) {
|
||||
className = "http-status-info";
|
||||
} else if (code >= 200 && code <= 299) {
|
||||
className = "http-status-success";
|
||||
} else if (code >= 300 && code <= 399) {
|
||||
className = "http-status-redirection";
|
||||
} else if (code >= 400 && code <= 499) {
|
||||
className = "http-status-client-error";
|
||||
} else if (code >= 500 && code <= 599) {
|
||||
className = "http-status-server-error";
|
||||
}
|
||||
|
||||
return className;
|
||||
},
|
||||
|
||||
statusIcon: function () {
|
||||
const code = this.document.http_status_code;
|
||||
let icon = null;
|
||||
|
||||
if (code === 0) {
|
||||
icon = "error";
|
||||
} else if (code >= 100 && code <= 199) {
|
||||
icon = "info";
|
||||
} else if (code >= 200 && code <= 299) {
|
||||
icon = "success";
|
||||
} else if (code >= 300 && code <= 399) {
|
||||
icon = "redirect";
|
||||
} else if (code >= 400 && code <= 499) {
|
||||
icon = "warning";
|
||||
} else if (code >= 500 && code <= 599) {
|
||||
icon = "error";
|
||||
}
|
||||
|
||||
return icon;
|
||||
},
|
||||
|
||||
metaData: function () {
|
||||
return collect(this.document.meta_data);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
document: function (document) {
|
||||
const self = this;
|
||||
|
||||
if (document && document.id) {
|
||||
self.load(document).then(function () {
|
||||
self.findDupplicateInFolders();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.document && self.document.id) {
|
||||
self.load(self.document).then(function () {
|
||||
self.findDupplicateInFolders();
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
incrementVisits: "documents/incrementVisits",
|
||||
openDocument: "documents/openDocument",
|
||||
ignoreFeed: "documents/ignoreFeed",
|
||||
followFeed: "documents/followFeed",
|
||||
load: "documents/load",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Filters dupplicates provided by the document by excluding current
|
||||
* folder
|
||||
*/
|
||||
findDupplicateInFolders: function () {
|
||||
const self = this;
|
||||
|
||||
const dupplicates = collect(self.document.dupplicates)
|
||||
.reject((folder) => folder.id === self.selectedFolder.id)
|
||||
.all();
|
||||
|
||||
self.dupplicateInFolders = dupplicates;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark as read button clicked
|
||||
*/
|
||||
onMarkAsReadClicked: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("feeditems-read", {
|
||||
documents: [self.document.id],
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete button clicked
|
||||
*/
|
||||
onDeleteDocument: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("documents-deleted", {
|
||||
folder: self.selectedFolder,
|
||||
documents: [self.document],
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Share button clicked
|
||||
*/
|
||||
onShareClicked: function () {
|
||||
const self = this;
|
||||
const sharedData = {
|
||||
title: self.document.title,
|
||||
text: self.document.description,
|
||||
url: self.document.url,
|
||||
};
|
||||
|
||||
if (navigator.share) {
|
||||
navigator.share(sharedData);
|
||||
} else {
|
||||
location.href =
|
||||
"mailto:?subject=" +
|
||||
sharedData.title +
|
||||
"&body=" +
|
||||
sharedData.url;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Follow specified feed
|
||||
*/
|
||||
follow: function (feed) {
|
||||
const self = this;
|
||||
|
||||
self.followFeed(feed);
|
||||
},
|
||||
|
||||
/**
|
||||
* Ignore specified feed
|
||||
*/
|
||||
ignore: function (feed) {
|
||||
const self = this;
|
||||
|
||||
self.ignoreFeed(feed);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
118
resources/js/components/Details/DetailsDocuments.vue
Executable file
118
resources/js/components/Details/DetailsDocuments.vue
Executable file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<article>
|
||||
<h1>
|
||||
<span></span>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
v-if="totalUnreadFeedItemsCount > 0"
|
||||
class="button info"
|
||||
v-on:click="onMarkAsReadClicked"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('unread_items')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Mark as read") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="info"
|
||||
v-on:click.left.stop.prevent="onOpenClicked"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('open')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Open") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div class="body">
|
||||
<img
|
||||
v-for="document in documents"
|
||||
v-bind:key="document.id"
|
||||
v-bind:title="document.title"
|
||||
v-bind:src="document.favicon"
|
||||
class="favicon inline mr-1 mb-1"
|
||||
/>
|
||||
<div
|
||||
class="mt-6"
|
||||
v-if="
|
||||
selectedFolder.type !== 'unread_items' &&
|
||||
selectedFolder.user_permissions.can_delete_document
|
||||
"
|
||||
>
|
||||
<button class="danger" v-on:click="onDeleteDocument">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('trash')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Delete") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
documents: "documents/selectedDocuments",
|
||||
selectedFolder: "folders/selectedFolder",
|
||||
}),
|
||||
totalUnreadFeedItemsCount: function () {
|
||||
const self = this;
|
||||
|
||||
return collect(self.documents).sum("feed_item_states_count");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
openDocument: "documents/openDocument",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Click on the "Open" button
|
||||
*/
|
||||
onOpenClicked: function () {
|
||||
const self = this;
|
||||
|
||||
self.documents.forEach(function (document) {
|
||||
self.openDocument({
|
||||
document: document,
|
||||
folder: self.selectedFolder,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark as read button clicked
|
||||
*/
|
||||
onMarkAsReadClicked: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("feeditems-read", {
|
||||
documents: collect(self.documents).pluck("id").all(),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete button clicked
|
||||
*/
|
||||
onDeleteDocument: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("documents-deleted", {
|
||||
folder: self.selectedFolder,
|
||||
documents: self.documents,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
196
resources/js/components/Details/DetailsFeedItem.vue
Executable file
196
resources/js/components/Details/DetailsFeedItem.vue
Executable file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<article>
|
||||
<header>
|
||||
<h1>
|
||||
<span v-html="feedItem.title"></span>
|
||||
</h1>
|
||||
<div class="tools">
|
||||
<button
|
||||
v-if="feedItem.feed_item_states_count > 0"
|
||||
class="button info"
|
||||
v-on:click="onMarkAsReadClicked"
|
||||
v-bind:title="__('Mark as read')"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('unread_items')" />
|
||||
</svg>
|
||||
<span class="hidden xl:inline-block">
|
||||
{{ __("Mark as read") }}
|
||||
</span>
|
||||
</button>
|
||||
<a
|
||||
class="button info"
|
||||
v-bind:href="feedItem.url"
|
||||
rel="noopener noreferrer"
|
||||
v-bind:title="__('Open')"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('open')" />
|
||||
</svg>
|
||||
<span class="hidden xl:inline-block">
|
||||
{{ __("Open") }}
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
class="button info"
|
||||
v-on:click="onShareClicked"
|
||||
v-bind:title="__('Share')"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('share')" />
|
||||
</svg>
|
||||
<span class="hidden xl:inline-block">
|
||||
{{ __("Share") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="body">
|
||||
<div
|
||||
class="cyca-prose"
|
||||
v-html="
|
||||
feedItem.content ? feedItem.content : feedItem.description
|
||||
"
|
||||
></div>
|
||||
|
||||
<stateful-details name="feed_item_details" class="details-block">
|
||||
<summary>{{ __("Details") }}</summary>
|
||||
<div class="vertical list striped items-rounded compact">
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">{{ __("Real URL") }}</div>
|
||||
<div class="list-item-value">
|
||||
<a
|
||||
v-bind:href="feedItem.url"
|
||||
rel="noopener noreferrer"
|
||||
class="readable"
|
||||
v-html="feedItem.ascii_url"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">
|
||||
{{ __("Date of item's creation") }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
<date-time
|
||||
v-bind:datetime="feedItem.created_at"
|
||||
v-bind:calendar="true"
|
||||
></date-time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">
|
||||
{{ __("Date of item's publication") }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
<date-time
|
||||
v-bind:datetime="feedItem.published_at"
|
||||
v-bind:calendar="true"
|
||||
></date-time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-item">
|
||||
<div class="list-item-title">
|
||||
{{ __("Published in") }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
<div class="list horizontal compact">
|
||||
<div
|
||||
class="list-item px-0 ml-2"
|
||||
v-for="feed in feedItem.feeds"
|
||||
v-bind:key="feed.id"
|
||||
v-bind:title="feed.url"
|
||||
>
|
||||
<div class="icons">
|
||||
<img v-bind:src="feed.favicon" />
|
||||
</div>
|
||||
<div class="list-item-text">
|
||||
{{ feed.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</stateful-details>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
import DateTime from "../DateTime";
|
||||
import StatefulDetails from "../StatefulDetails.vue";
|
||||
|
||||
export default {
|
||||
components: { DateTime, StatefulDetails },
|
||||
/**
|
||||
* Computed properties
|
||||
*/
|
||||
computed: {
|
||||
...mapGetters({
|
||||
feedItem: "feedItems/selectedFeedItem",
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
feedItem: function () {
|
||||
this.$el.getElementsByClassName("body")[0].scrollTop = 0;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
...mapActions({
|
||||
loadFolders: "folders/loadFolders",
|
||||
loadFeedItemDetails: "feedItems/loadDetails",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Mark as read button clicked
|
||||
*/
|
||||
onMarkAsReadClicked: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("feeditems-read", {
|
||||
feed_items: [self.feedItem.id],
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Share button clicked
|
||||
*/
|
||||
onShareClicked: function () {
|
||||
const self = this;
|
||||
|
||||
var content = self.feedItem.description
|
||||
? self.feedItem.description
|
||||
: self.feedItem.content;
|
||||
|
||||
if (content) {
|
||||
content = content.substring(0, 200);
|
||||
}
|
||||
|
||||
const sharedData = {
|
||||
title: self.feedItem.title,
|
||||
text: content,
|
||||
url: self.feedItem.url,
|
||||
};
|
||||
|
||||
if (navigator.share) {
|
||||
navigator.share(sharedData);
|
||||
} else {
|
||||
location.href =
|
||||
"mailto:?subject=" +
|
||||
sharedData.title +
|
||||
"&body=" +
|
||||
sharedData.url;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
297
resources/js/components/Details/DetailsFolder.vue
Executable file
297
resources/js/components/Details/DetailsFolder.vue
Executable file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<article v-if="folder">
|
||||
<header>
|
||||
<div class="icons">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
v-bind:class="folder.iconColor"
|
||||
>
|
||||
<use v-bind:xlink:href="icon(folder.icon)" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>{{ folder.title }}</h1>
|
||||
<div class="badges">
|
||||
<div
|
||||
class="badge default"
|
||||
v-if="folder.feed_item_states_count > 0"
|
||||
>
|
||||
{{ folder.feed_item_states_count }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tools">
|
||||
<button
|
||||
v-if="folder.feed_item_states_count > 0"
|
||||
class="info"
|
||||
v-on:click="onMarkAsReadClicked"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('unread_items')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Mark as read") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="body">
|
||||
<form
|
||||
v-bind:action="route('folder.update', folder)"
|
||||
v-if="can('can_update_folder') && folder.type === 'folder'"
|
||||
v-on:submit.prevent="onUpdateFolder"
|
||||
>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
v-bind:value="folder.title"
|
||||
v-on:input="updateFolderTitle = $event.target.value"
|
||||
/>
|
||||
<button type="submit" class="success">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('update')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Update folder") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form
|
||||
v-bind:action="route('folder.store')"
|
||||
v-on:submit.prevent="onAddFolder"
|
||||
v-if="can('can_create_folder')"
|
||||
>
|
||||
<div class="input-group">
|
||||
<input type="text" v-model="addFolderTitle" />
|
||||
<button type="submit" class="success">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('add')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Add folder") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form
|
||||
v-bind:action="route('document.store')"
|
||||
v-on:submit.prevent="onAddDocument"
|
||||
v-if="can('can_create_document')"
|
||||
class="mb-6"
|
||||
>
|
||||
<div class="input-group">
|
||||
<input type="url" v-model="addDocumentUrl" />
|
||||
<button type="submit" class="success">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('add')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Add document") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
v-if="folder.group && folder.group.active_users_count > 1"
|
||||
class="mb-6"
|
||||
>
|
||||
<default-folder-permissions
|
||||
class="details-block"
|
||||
v-bind:folder="folder"
|
||||
v-if="
|
||||
can('can_change_permissions') &&
|
||||
(folder.type === 'root' || folder.type === 'folder')
|
||||
"
|
||||
></default-folder-permissions>
|
||||
|
||||
<per-user-folder-permissions
|
||||
class="details-block"
|
||||
v-bind:folder="folder"
|
||||
v-if="
|
||||
can('can_change_permissions') &&
|
||||
(folder.type === 'root' || folder.type === 'folder')
|
||||
"
|
||||
></per-user-folder-permissions>
|
||||
</div>
|
||||
|
||||
<div v-if="can('can_delete_folder')">
|
||||
<button class="danger" v-on:click="onDeleteFolder">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('trash')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Delete") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import DefaultFolderPermissions from "./DefaultFolderPermissions.vue";
|
||||
import PerUserFolderPermissions from "./PerUserFolderPermissions.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DefaultFolderPermissions,
|
||||
PerUserFolderPermissions,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
updateFolderTitle: null,
|
||||
addFolderTitle: null,
|
||||
addDocumentUrl: null,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Computed properties
|
||||
*/
|
||||
computed: {
|
||||
...mapGetters({
|
||||
group: "groups/selectedGroup",
|
||||
folder: "folders/selectedFolder",
|
||||
}),
|
||||
},
|
||||
/**
|
||||
* Watchers
|
||||
*/
|
||||
watch: {
|
||||
folder: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.folder) {
|
||||
self.loadDetails(self.folder).then(function () {
|
||||
self.$forceUpdate();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.folder) {
|
||||
self.loadDetails(self.folder).then(function () {
|
||||
self.$forceUpdate();
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
...mapActions({
|
||||
update: "folders/update",
|
||||
updateProperties: "folders/updateProperties",
|
||||
updatePermission: "folders/updatePermission",
|
||||
store: "folders/store",
|
||||
destroy: "folders/destroy",
|
||||
storeDocument: "documents/store",
|
||||
index: "folders/index",
|
||||
loadDetails: "folders/loadDetails",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Mark as read button clicked
|
||||
*/
|
||||
onMarkAsReadClicked: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("feeditems-read", {
|
||||
folders: [self.folder.id],
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update folder form submitted
|
||||
*/
|
||||
onUpdateFolder: function () {
|
||||
const self = this;
|
||||
|
||||
if (
|
||||
!self.updateFolderTitle ||
|
||||
self.updateFolderTitle === self.folder.title
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.update({
|
||||
folder: self.folder,
|
||||
newProperties: {
|
||||
...self.folder,
|
||||
...{
|
||||
title: self.updateFolderTitle,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Add folder form submitted
|
||||
*/
|
||||
onAddFolder: function () {
|
||||
const self = this;
|
||||
|
||||
if (!self.addFolderTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.store({
|
||||
group_id: self.group.id,
|
||||
parent_id: self.folder.id,
|
||||
title: self.addFolderTitle,
|
||||
})
|
||||
.then(function () {
|
||||
self.addFolderTitle = null;
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
},
|
||||
/**
|
||||
* Delete folder button clicked
|
||||
*/
|
||||
onDeleteFolder: function () {
|
||||
const self = this;
|
||||
|
||||
self.destroy(self.folder);
|
||||
},
|
||||
/**
|
||||
* Add document form submitted
|
||||
*/
|
||||
onAddDocument: function () {
|
||||
const self = this;
|
||||
|
||||
if (!self.addDocumentUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.storeDocument({
|
||||
group_id: self.group.id,
|
||||
folder_id: self.folder.id,
|
||||
url: self.addDocumentUrl,
|
||||
}).then(function () {
|
||||
self.addDocumentUrl = null;
|
||||
self.$emit("document-added");
|
||||
});
|
||||
},
|
||||
|
||||
can: function (permission) {
|
||||
const self = this;
|
||||
|
||||
if (
|
||||
"user_permissions" in self.folder &&
|
||||
permission in self.folder.user_permissions
|
||||
) {
|
||||
return self.folder.user_permissions[permission];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
170
resources/js/components/Details/PerUserFolderPermissions.vue
Executable file
170
resources/js/components/Details/PerUserFolderPermissions.vue
Executable file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<stateful-details
|
||||
name="per_user_folders_permissions_details"
|
||||
v-on:toggle="onToggle"
|
||||
>
|
||||
<summary>{{ __("Per-user permissions") }}:</summary>
|
||||
|
||||
<div class="body">
|
||||
<form v-if="usersWithoutPermissions.length > 0">
|
||||
<div class="input-group">
|
||||
<label for="addPermissionsForUser"
|
||||
>{{ __("Add explicit permissions for") }}:</label
|
||||
>
|
||||
<select
|
||||
v-model="addPermissionsForUser"
|
||||
id="addPermissionsForUser"
|
||||
>
|
||||
<option
|
||||
v-for="user in usersWithoutPermissions"
|
||||
v-bind:key="user.id"
|
||||
v-bind:value="user.id"
|
||||
>
|
||||
{{ user.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
class="list vertical spaced striped items-rounded compact"
|
||||
v-for="user in users"
|
||||
v-bind:key="user.id"
|
||||
>
|
||||
<div class="list-item">
|
||||
<div>
|
||||
{{ user.name }}
|
||||
<small class="feed-item-meta inline ml-2">{{
|
||||
user.email
|
||||
}}</small>
|
||||
<div class="flex items-center space-x-2 mt-2">
|
||||
<permission-box
|
||||
v-bind:text="__('Create folder')"
|
||||
ability="can_create_folder"
|
||||
v-bind:folder="folder"
|
||||
v-bind:user="user"
|
||||
></permission-box>
|
||||
<permission-box
|
||||
v-bind:text="__('Update folder')"
|
||||
ability="can_update_folder"
|
||||
v-bind:folder="folder"
|
||||
v-bind:user="user"
|
||||
></permission-box>
|
||||
<permission-box
|
||||
v-bind:text="__('Delete folder')"
|
||||
ability="can_delete_folder"
|
||||
v-bind:folder="folder"
|
||||
v-bind:user="user"
|
||||
></permission-box>
|
||||
<permission-box
|
||||
v-bind:text="__('Create document')"
|
||||
ability="can_create_document"
|
||||
v-bind:folder="folder"
|
||||
v-bind:user="user"
|
||||
></permission-box>
|
||||
<permission-box
|
||||
v-bind:text="__('Delete document')"
|
||||
ability="can_delete_document"
|
||||
v-bind:folder="folder"
|
||||
v-bind:user="user"
|
||||
></permission-box>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="info"
|
||||
v-on:click="onRemovePermissions(user)"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('cancel')" /></svg
|
||||
><span>{{ __("Apply default permissions") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</stateful-details>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PermissionBox from "./PermissionBox.vue";
|
||||
import StatefulDetails from "../StatefulDetails.vue";
|
||||
|
||||
export default {
|
||||
components: { PermissionBox, StatefulDetails },
|
||||
props: ["folder"],
|
||||
data: function () {
|
||||
return {
|
||||
users: [],
|
||||
usersWithoutPermissions: [],
|
||||
addPermissionsForUser: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
folder: function (folder) {
|
||||
this.loadPermissions();
|
||||
},
|
||||
addPermissionsForUser: function (userId) {
|
||||
const self = this;
|
||||
|
||||
if (userId) {
|
||||
api.post(
|
||||
route("folder.set_permission", { folder: self.folder.id }),
|
||||
{
|
||||
user_id: userId,
|
||||
}
|
||||
).then(function (response) {
|
||||
self.users = response;
|
||||
self.addPermissionsForUser = null;
|
||||
|
||||
self.loadUsersWithoutPermissions();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onToggle: function (evt) {
|
||||
if (evt.target.open) {
|
||||
this.loadPermissions();
|
||||
}
|
||||
},
|
||||
loadPermissions: function () {
|
||||
this.loadPerUserPermissions();
|
||||
this.loadUsersWithoutPermissions();
|
||||
},
|
||||
loadPerUserPermissions: function () {
|
||||
const self = this;
|
||||
|
||||
api.get(route("folder.per_user_permissions", self.folder)).then(
|
||||
function (response) {
|
||||
self.users = response;
|
||||
}
|
||||
);
|
||||
},
|
||||
loadUsersWithoutPermissions: function () {
|
||||
const self = this;
|
||||
|
||||
api.get(
|
||||
route("folder.users_without_permissions", self.folder)
|
||||
).then(function (response) {
|
||||
self.usersWithoutPermissions = response;
|
||||
});
|
||||
},
|
||||
|
||||
onRemovePermissions: function (user) {
|
||||
const self = this;
|
||||
|
||||
api.delete(
|
||||
route("folder.remove_permissions", {
|
||||
folder: self.folder.id,
|
||||
user: user.id,
|
||||
})
|
||||
).then(function (response) {
|
||||
self.users = response;
|
||||
|
||||
self.loadUsersWithoutPermissions();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
80
resources/js/components/Details/PermissionBox.vue
Executable file
80
resources/js/components/Details/PermissionBox.vue
Executable file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<label
|
||||
class="badge cursor-pointer"
|
||||
v-bind:class="{ success: granted, danger: !granted }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="granted"
|
||||
class="hidden"
|
||||
v-on:input="
|
||||
updatePermission({
|
||||
ability: ability,
|
||||
granted: $event.target.checked,
|
||||
folder: folder,
|
||||
user: user ? user.id : null,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use
|
||||
v-bind:xlink:href="
|
||||
granted
|
||||
? icon('permission_granted')
|
||||
: icon('permission_denied')
|
||||
"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{{ text }}
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from "vuex";
|
||||
export default {
|
||||
props: ["folder", "user", "ability", "text"],
|
||||
data: function () {
|
||||
return {
|
||||
granted: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
folder: function () {
|
||||
this.granted = this.canByDefault(this.ability);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
user: function () {
|
||||
this.granted = this.canByDefault(this.ability);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
this.granted = this.canByDefault(this.ability);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
updatePermission: "folders/updatePermission",
|
||||
}),
|
||||
canByDefault: function (permission) {
|
||||
const self = this;
|
||||
|
||||
if (
|
||||
self.user &&
|
||||
"permissions" in self.user &&
|
||||
permission in self.user.permissions[0]
|
||||
) {
|
||||
return self.user.permissions[0][permission];
|
||||
} else if (
|
||||
"default_permissions" in self.folder &&
|
||||
permission in self.folder.default_permissions
|
||||
) {
|
||||
return self.folder.default_permissions[permission];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
217
resources/js/components/DocumentItem.vue
Executable file
217
resources/js/components/DocumentItem.vue
Executable file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<a
|
||||
class="list-item"
|
||||
v-bind:class="{ selected: is_selected }"
|
||||
v-bind:draggable="true"
|
||||
v-bind:href="url"
|
||||
v-on:click.meta.left.exact.prevent.stop="onAddToSelection"
|
||||
v-on:click.left.exact.prevent.stop="onClicked"
|
||||
v-on:click.middle.exact="
|
||||
incrementVisits({ document: document, folder: selectedFolder })
|
||||
"
|
||||
v-on:dblclick="
|
||||
openDocument({ document: document, folder: selectedFolder })
|
||||
"
|
||||
v-on:dragstart="onDragStart"
|
||||
v-on:dragend="onDragEnd"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div class="icons">
|
||||
<img v-bind:src="document.favicon" class="favicon-lg" />
|
||||
</div>
|
||||
<div class="list-item-text">
|
||||
{{ document.title }}
|
||||
<div class="text-sm">{{ document.url }}</div>
|
||||
</div>
|
||||
<div class="badges">
|
||||
<div
|
||||
v-if="
|
||||
document.http_status_code < 200 ||
|
||||
document.http_status_code > 299
|
||||
"
|
||||
v-bind:class="statusClass"
|
||||
v-bind:title="document.http_status_text"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon(statusIcon)" />
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="badge default"
|
||||
v-if="document.feed_item_states_count > 0"
|
||||
>
|
||||
<span v-if="document.has_new_unread_items">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
class="text-blue-300"
|
||||
>
|
||||
<use v-bind:xlink:href="icon('update')" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ document.feed_item_states_count }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
props: ["document"],
|
||||
data: function () {
|
||||
return {
|
||||
enableEnsureVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedDocuments: "documents/selectedDocuments",
|
||||
selectedFolder: "folders/selectedFolder",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Return a boolean value indicating if this document belongs to
|
||||
* currently selected documents
|
||||
*/
|
||||
is_selected: function () {
|
||||
const self = this;
|
||||
|
||||
return (
|
||||
self.selectedDocuments &&
|
||||
self.selectedDocuments.find((d) => d.id === self.document.id)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return document's initial URL instead of the real URL, unless there
|
||||
* is not attached bookmark
|
||||
*/
|
||||
url: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.document.bookmark.initial_url) {
|
||||
return self.document.bookmark.initial_url;
|
||||
}
|
||||
|
||||
return self.document.url;
|
||||
},
|
||||
|
||||
statusClass: function () {
|
||||
const code = this.document.http_status_code;
|
||||
let className = null;
|
||||
|
||||
if (code === 0) {
|
||||
className = "http-status-general-error";
|
||||
} else if (code >= 100 && code <= 199) {
|
||||
className = "http-status-info";
|
||||
} else if (code >= 200 && code <= 299) {
|
||||
className = "http-status-success";
|
||||
} else if (code >= 300 && code <= 399) {
|
||||
className = "http-status-redirection";
|
||||
} else if (code >= 400 && code <= 499) {
|
||||
className = "http-status-client-error";
|
||||
} else if (code >= 500 && code <= 599) {
|
||||
className = "http-status-server-error";
|
||||
}
|
||||
|
||||
return className;
|
||||
},
|
||||
|
||||
statusIcon: function () {
|
||||
const code = this.document.http_status_code;
|
||||
let icon = null;
|
||||
|
||||
if (code === 0) {
|
||||
icon = "error";
|
||||
} else if (code >= 100 && code <= 199) {
|
||||
icon = "info";
|
||||
} else if (code >= 200 && code <= 299) {
|
||||
icon = "success";
|
||||
} else if (code >= 300 && code <= 399) {
|
||||
icon = "redirect";
|
||||
} else if (code >= 400 && code <= 499) {
|
||||
icon = "warning";
|
||||
} else if (code >= 500 && code <= 599) {
|
||||
icon = "error";
|
||||
}
|
||||
|
||||
return icon;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
startDraggingDocuments: "documents/startDraggingDocuments",
|
||||
stopDraggingDocuments: "documents/stopDraggingDocuments",
|
||||
incrementVisits: "documents/incrementVisits",
|
||||
openDocument: "documents/openDocument",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Document has been clicked
|
||||
*/
|
||||
onClicked: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("selected-documents-changed", [self.document]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add this document to selection
|
||||
*/
|
||||
onAddToSelection: function () {
|
||||
const self = this;
|
||||
|
||||
let selectedDocuments = [...self.selectedDocuments];
|
||||
|
||||
const index = selectedDocuments.findIndex(
|
||||
(doc) => doc.id === self.document.id
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
selectedDocuments.push(self.document);
|
||||
} else {
|
||||
selectedDocuments.splice(index, 1);
|
||||
}
|
||||
|
||||
self.$emit("selected-documents-changed", selectedDocuments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Begin drag'n'drop
|
||||
*/
|
||||
onDragStart: function () {
|
||||
const self = this;
|
||||
|
||||
var documents = [self.document];
|
||||
|
||||
if (self.selectedDocuments && self.selectedDocuments.length > 0) {
|
||||
documents = self.selectedDocuments;
|
||||
}
|
||||
|
||||
self.startDraggingDocuments(documents);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop drag'n'drop
|
||||
*/
|
||||
onDragEnd: function () {
|
||||
const self = this;
|
||||
|
||||
self.stopDraggingDocuments();
|
||||
},
|
||||
|
||||
ensureVisible: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.enableEnsureVisible) {
|
||||
self.$el.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
59
resources/js/components/DocumentsList.vue
Executable file
59
resources/js/components/DocumentsList.vue
Executable file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="vertical list spaced striped items-rounded">
|
||||
<document-item
|
||||
v-for="document in sortedList"
|
||||
v-bind:key="document.id"
|
||||
v-bind:document="document"
|
||||
v-on:selected-documents-changed="onSelectedDocumentsChanged"
|
||||
></document-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
import DocumentItem from "./DocumentItem";
|
||||
|
||||
export default {
|
||||
components: { DocumentItem },
|
||||
data: function () {
|
||||
return {
|
||||
selectedFeeds: [],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Computed properties
|
||||
*/
|
||||
computed: {
|
||||
...mapGetters({
|
||||
documents: "documents/documents",
|
||||
selectedFolder: "folders/selectedFolder",
|
||||
}),
|
||||
sortedList: function () {
|
||||
const self = this;
|
||||
let collection = collect(self.documents).sortBy("title");
|
||||
|
||||
if (
|
||||
self.selectedFolder &&
|
||||
self.selectedFolder.type === "unread_items"
|
||||
) {
|
||||
collection = collection.where("feed_item_states_count", ">", 0);
|
||||
}
|
||||
|
||||
return collection.all();
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Selected documents have changed
|
||||
*/
|
||||
onSelectedDocumentsChanged: function (documents) {
|
||||
const self = this;
|
||||
|
||||
self.$emit("selected-documents-changed", documents);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
153
resources/js/components/FeedItem.vue
Executable file
153
resources/js/components/FeedItem.vue
Executable file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<a
|
||||
v-bind:class="{
|
||||
selected: is_selected,
|
||||
emphasize: feedItem.feed_item_states_count > 0,
|
||||
}"
|
||||
class="list-item"
|
||||
v-bind:href="feedItem.url"
|
||||
rel="noopener noreferrer"
|
||||
v-on:click.left.exact.stop.prevent="onClicked"
|
||||
>
|
||||
<div
|
||||
class="list-item-text mr-2"
|
||||
v-html="highlight(feedItem.title)"
|
||||
></div>
|
||||
<div class="flex justify-between items-center space-x-2">
|
||||
<div class="list horizontal compact overflow-hidden">
|
||||
<div
|
||||
class="list-item px-0"
|
||||
v-bind:title="feedItem.feeds[0].title"
|
||||
>
|
||||
<div class="icons">
|
||||
<img v-bind:src="feedItem.feeds[0].favicon" />
|
||||
</div>
|
||||
<div class="hidden xl:inline-block text-xs">
|
||||
{{ feedItem.feeds[0].title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<date-time
|
||||
class="text-2xs font-mono badge shadow dark:bg-gray-850"
|
||||
v-bind:datetime="feedItem.published_at"
|
||||
v-bind:calendar="true"
|
||||
></date-time>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TEXTColor from "textcolor";
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
import DateTime from "./DateTime";
|
||||
|
||||
export default {
|
||||
components: { DateTime },
|
||||
data: function () {
|
||||
return {
|
||||
enableEnsureVisible: true,
|
||||
};
|
||||
},
|
||||
props: ["feedItem"],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedFeedItems: "feedItems/selectedFeedItems",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Return a boolean value indicating if this document belongs to
|
||||
* currently selected documents
|
||||
*/
|
||||
is_selected: function () {
|
||||
const self = this;
|
||||
|
||||
let selected = false;
|
||||
|
||||
try {
|
||||
selected =
|
||||
self.selectedFeedItems &&
|
||||
self.selectedFeedItems.find(
|
||||
(fi) => fi.id === self.feedItem.id
|
||||
);
|
||||
} finally {
|
||||
return selected;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
is_selected: function (selected) {
|
||||
const self = this;
|
||||
|
||||
if (selected) {
|
||||
self.ensureVisible();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
selectFeedItem: "feedItems/selectFeedItem",
|
||||
addFeedItemToSelection: "documents/addFeedItemToSelection",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Document has been clicked
|
||||
*/
|
||||
onClicked: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("selected-feeditems-changed", [self.feedItem]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add this document to selection
|
||||
*/
|
||||
onAddToSelection: function () {
|
||||
const self = this;
|
||||
|
||||
let selectedFeedItems = [...self.selectedFeedItems];
|
||||
|
||||
const index = selectedFeedItems.findIndex(
|
||||
(fi) => fi.id === self.feedItem.id
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
selectedFeedItems.push(self.feedItem);
|
||||
} else {
|
||||
selectedFeedItems.splice(index, 1);
|
||||
}
|
||||
|
||||
self.$emit("selected-feeditems-changed", selectedFeedItems);
|
||||
},
|
||||
|
||||
highlight: function (title) {
|
||||
highlights.forEach(function (highlight) {
|
||||
var regex = new RegExp(
|
||||
"(" + highlight.expression + ")(?![^<]*>|[^<>]*</)",
|
||||
"ig"
|
||||
);
|
||||
let textColor = TEXTColor.findTextColor(highlight.color);
|
||||
title = title.replace(
|
||||
regex,
|
||||
'<span class="highlight" style="color: ' +
|
||||
textColor +
|
||||
"; background-color: " +
|
||||
highlight.color +
|
||||
'">$1</span>'
|
||||
);
|
||||
});
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
ensureVisible: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.enableEnsureVisible) {
|
||||
self.$el.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
90
resources/js/components/FeedItemsList.vue
Executable file
90
resources/js/components/FeedItemsList.vue
Executable file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div
|
||||
id="feed-items-list"
|
||||
class="list vertical spaced striped items-rounded"
|
||||
v-on:scroll.passive="onScroll"
|
||||
>
|
||||
<feed-item
|
||||
v-for="feedItem in sortedList"
|
||||
v-bind:key="feedItem.id"
|
||||
v-bind:feedItem="feedItem"
|
||||
v-on:selected-feeditems-changed="onSelectedFeedItemsChanged"
|
||||
></feed-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
import FeedItem from "./FeedItem";
|
||||
|
||||
export default {
|
||||
components: { FeedItem },
|
||||
/**
|
||||
* Computed properties
|
||||
*/
|
||||
computed: {
|
||||
...mapGetters({
|
||||
feedItems: "feedItems/feedItems",
|
||||
canLoadMore: "feedItems/canLoadMore",
|
||||
selectedFolder: "folders/selectedFolder",
|
||||
}),
|
||||
/**
|
||||
* Return list of feed items sorted by published date
|
||||
*/
|
||||
sortedList: function () {
|
||||
const self = this;
|
||||
let collection = collect(self.feedItems).sortByDesc("published_at");
|
||||
|
||||
if (
|
||||
self.selectedFolder &&
|
||||
self.selectedFolder.type === "unread_items"
|
||||
) {
|
||||
collection = collection.where("feed_item_states_count", ">", 0);
|
||||
}
|
||||
|
||||
return collection.all();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sortedList: function () {
|
||||
const self = this;
|
||||
|
||||
self.$nextTick(function () {
|
||||
const scrollHeight = self.$el.scrollHeight;
|
||||
const innerHeight = self.$el.clientHeight;
|
||||
|
||||
if (scrollHeight === innerHeight && self.canLoadMore) {
|
||||
self.loadMoreFeedItems();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
loadMoreFeedItems: "feedItems/loadMoreFeedItems",
|
||||
}),
|
||||
|
||||
/**
|
||||
* Selected feed items has changed
|
||||
*/
|
||||
onSelectedFeedItemsChanged: function (feedItems) {
|
||||
const self = this;
|
||||
|
||||
self.$emit("selected-feeditems-changed", feedItems);
|
||||
},
|
||||
|
||||
onScroll: function ($event) {
|
||||
const scrollTop = $event.target.scrollTop;
|
||||
const innerHeight = $event.target.clientHeight;
|
||||
const scrollHeight = $event.target.scrollHeight;
|
||||
|
||||
if (
|
||||
scrollTop + innerHeight >= scrollHeight - 20 &&
|
||||
this.canLoadMore
|
||||
) {
|
||||
this.loadMoreFeedItems();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
286
resources/js/components/FolderItem.vue
Executable file
286
resources/js/components/FolderItem.vue
Executable file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<a
|
||||
class="list-item compact"
|
||||
v-bind:class="{
|
||||
selected: folder.is_selected,
|
||||
'dragged-over': is_dragged_over,
|
||||
'cannot-drop': is_dragged_over && cannot_drop,
|
||||
deleted: folder.deleted_at,
|
||||
}"
|
||||
v-bind:href="route('folder.show', folder)"
|
||||
v-bind:draggable="isDraggable"
|
||||
v-on:click.stop.prevent="onClick"
|
||||
v-on:dragstart="onDragStart"
|
||||
v-on:dragend="onDragEnd"
|
||||
v-on:drop="onDrop"
|
||||
v-on:dragleave="onDragLeave"
|
||||
v-on:dragover="onDragOver"
|
||||
v-if="branchIsExpanded"
|
||||
v-bind:style="'padding-left:' + indent"
|
||||
>
|
||||
<div class="icons">
|
||||
<span class="caret">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
v-on:mouseup.capture.stop="onToggleExpandedClicked"
|
||||
v-on:mousedown.capture.stop="startTimer"
|
||||
v-if="folder.children_count > 0"
|
||||
>
|
||||
<use v-bind:xlink:href="icon(expanderIcon)" />
|
||||
</svg>
|
||||
</span>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
class="favicon"
|
||||
v-bind:class="folder.iconColor"
|
||||
>
|
||||
<use v-bind:xlink:href="icon(folder.icon)" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="list-item-text">{{ folder.title }}</div>
|
||||
<div class="badges">
|
||||
<div class="badge default" v-if="folder.feed_item_states_count > 0">
|
||||
<span v-if="folder.has_new_unread_items">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
class="text-blue-300"
|
||||
>
|
||||
<use v-bind:xlink:href="icon('update')" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ folder.feed_item_states_count }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
props: ["folder"],
|
||||
data: function () {
|
||||
return {
|
||||
is_dragged_over: false,
|
||||
cannot_drop: false,
|
||||
timer: null,
|
||||
longClick: false,
|
||||
enableEnsureVisible: false,
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.folder.is_selected) {
|
||||
self.enableEnsureVisible = true;
|
||||
self.ensureVisible();
|
||||
self.enableEnsureVisible = false;
|
||||
}
|
||||
|
||||
self.$watch(
|
||||
() => self.folder.is_selected,
|
||||
function (value) {
|
||||
if (value) {
|
||||
if (self.folder.type === "unread_items") {
|
||||
self.enableEnsureVisible = true;
|
||||
}
|
||||
|
||||
self.ensureVisible();
|
||||
self.enableEnsureVisible = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Computed properties
|
||||
*/
|
||||
computed: {
|
||||
...mapGetters({
|
||||
folders: "folders/folders",
|
||||
}),
|
||||
/**
|
||||
* Folder's indentation
|
||||
*/
|
||||
indent: function () {
|
||||
return this.folder.depth + "rem";
|
||||
},
|
||||
/**
|
||||
* Return a boolean value indicating if folder can be dragged
|
||||
*/
|
||||
isDraggable: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.folder.deleted_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self.folder.type === "folder";
|
||||
},
|
||||
/**
|
||||
* Return a boolean value indicating if content can be dropped into this
|
||||
* folder
|
||||
*/
|
||||
canDrop: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.folder.deleted_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self.folder.type === "folder" || self.folder.type === "root";
|
||||
},
|
||||
/**
|
||||
* Return a boolean value indicating if branch is expanded or collapsed
|
||||
*/
|
||||
branchIsExpanded: function () {
|
||||
const self = this;
|
||||
var parentId = self.folder.parent_id;
|
||||
|
||||
if (!parentId || !self.folders || self.folder.type === "root") {
|
||||
return true;
|
||||
}
|
||||
|
||||
while (parentId != null) {
|
||||
var parentFolder = self.folders.find((f) => f.id === parentId);
|
||||
|
||||
if (parentFolder) {
|
||||
if (!parentFolder.is_expanded) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
parentId = parentFolder.parent_id;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Icon adjacent to folder indicating its state (expanded/collapsed)
|
||||
*/
|
||||
expanderIcon: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.folder.is_expanded) {
|
||||
return "expanded";
|
||||
}
|
||||
|
||||
return "collapsed";
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
...mapActions({
|
||||
startDraggingFolder: "folders/startDraggingFolder",
|
||||
stopDraggingFolder: "folders/stopDraggingFolder",
|
||||
dropIntoFolder: "folders/dropIntoFolder",
|
||||
toggleExpanded: "folders/toggleExpanded",
|
||||
toggleBranch: "folders/toggleBranch",
|
||||
}),
|
||||
/**
|
||||
* Dragging started
|
||||
*/
|
||||
onDragStart: function (event) {
|
||||
const self = this;
|
||||
|
||||
self.startDraggingFolder(self.folder);
|
||||
},
|
||||
/**
|
||||
* Dragging ended
|
||||
*/
|
||||
onDragEnd: function () {
|
||||
const self = this;
|
||||
|
||||
self.stopDraggingFolder();
|
||||
|
||||
self.is_dragged_over = false;
|
||||
},
|
||||
/**
|
||||
* Dropping content in this folder
|
||||
*/
|
||||
onDrop: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("item-dropped", self.folder);
|
||||
|
||||
self.is_dragged_over = false;
|
||||
},
|
||||
/**
|
||||
* Dragging outside of this folder
|
||||
*/
|
||||
onDragLeave: function () {
|
||||
const self = this;
|
||||
|
||||
self.is_dragged_over = false;
|
||||
},
|
||||
/**
|
||||
* Dragging over this folder
|
||||
*/
|
||||
onDragOver: function (event) {
|
||||
const self = this;
|
||||
|
||||
self.is_dragged_over = true;
|
||||
|
||||
if (self.canDrop) {
|
||||
event.preventDefault();
|
||||
|
||||
self.cannot_drop = false;
|
||||
} else {
|
||||
self.cannot_drop = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Selected folder changed
|
||||
*/
|
||||
onClick: function () {
|
||||
const self = this;
|
||||
|
||||
self.$emit("selected-folder-changed", self.folder);
|
||||
},
|
||||
|
||||
/**
|
||||
* Keep track of current timestamp
|
||||
*/
|
||||
startTimer: function (event) {
|
||||
const self = this;
|
||||
|
||||
self.timer = window.setTimeout(function () {
|
||||
self.longClick = true;
|
||||
|
||||
self.toggleBranch(self.folder);
|
||||
}, 500);
|
||||
},
|
||||
|
||||
/**
|
||||
* Expand/collapse button clicked
|
||||
*/
|
||||
onToggleExpandedClicked: function (event) {
|
||||
const self = this;
|
||||
|
||||
if (!self.longClick) {
|
||||
self.toggleExpanded(self.folder);
|
||||
}
|
||||
|
||||
clearTimeout(self.timer);
|
||||
self.longClick = false;
|
||||
},
|
||||
|
||||
ensureVisible: function () {
|
||||
const self = this;
|
||||
|
||||
if (self.enableEnsureVisible) {
|
||||
self.$el.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
130
resources/js/components/FoldersTree.vue
Executable file
130
resources/js/components/FoldersTree.vue
Executable file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="list vertical items-rounded spaced compact striped flex-none overflow-auto max-h-36 bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<group-item
|
||||
v-for="group in sortedGroups"
|
||||
v-bind:key="group.id"
|
||||
v-bind:group="group"
|
||||
v-on:selected-group-changed="onSelectedGroupChanged"
|
||||
></group-item>
|
||||
</div>
|
||||
<div
|
||||
class="list vertical items-rounded overflow-auto flex-grow compact"
|
||||
>
|
||||
<folder-item
|
||||
v-for="folder in folders"
|
||||
v-bind:key="folder.id"
|
||||
v-bind:folder="folder"
|
||||
v-on:selected-folder-changed="onSelectedFolderChanged"
|
||||
v-on:item-dropped="onItemDropped"
|
||||
></folder-item>
|
||||
</div>
|
||||
<div
|
||||
class="list vertical items-rounded spaced compact striped flex-none bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<a v-bind:href="route('account')" class="list-item">
|
||||
<div class="icons">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
class="text-green-500"
|
||||
>
|
||||
<use v-bind:xlink:href="icon('account')" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="list-item-text">
|
||||
{{ __("My account") }}
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" class="list-item" v-on:click="logout">
|
||||
<div class="icons">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
class="text-red-500"
|
||||
>
|
||||
<use v-bind:xlink:href="icon('logout')" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="list-item-text">
|
||||
{{ __("Logout") }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import GroupItem from "./GroupItem.vue";
|
||||
import FolderItem from "./FolderItem.vue";
|
||||
|
||||
export default {
|
||||
components: { GroupItem, FolderItem },
|
||||
mounted: function () {
|
||||
const self = this;
|
||||
|
||||
self.indexGroups().then(function () {
|
||||
self.showGroup({});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Computed properties
|
||||
*/
|
||||
computed: {
|
||||
...mapGetters({
|
||||
folders: "folders/folders",
|
||||
groups: "groups/groups",
|
||||
}),
|
||||
csrf: function () {
|
||||
return document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
.getAttribute("content");
|
||||
},
|
||||
sortedGroups: function () {
|
||||
return collect(this.groups).sortBy("position");
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
...mapActions({
|
||||
indexGroups: "groups/indexActive",
|
||||
showGroup: "groups/show",
|
||||
}),
|
||||
|
||||
onSelectedGroupChanged: function (group) {
|
||||
const self = this;
|
||||
|
||||
self.$emit("selected-group-changed", group);
|
||||
},
|
||||
|
||||
/**
|
||||
* Folder has been selected
|
||||
*/
|
||||
onSelectedFolderChanged: function (folder) {
|
||||
const self = this;
|
||||
|
||||
self.$emit("selected-folder-changed", folder);
|
||||
},
|
||||
|
||||
/**
|
||||
* Something had been dropped into a folder
|
||||
*/
|
||||
onItemDropped: function (folder) {
|
||||
const self = this;
|
||||
|
||||
self.$emit("item-dropped", folder);
|
||||
},
|
||||
|
||||
logout: function () {
|
||||
document.forms.logout_form.submit();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
43
resources/js/components/GroupItem.vue
Executable file
43
resources/js/components/GroupItem.vue
Executable file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<a
|
||||
v-bind:href="route('group.show', group)"
|
||||
class="list-item"
|
||||
v-bind:class="{ selected: group.is_selected }"
|
||||
v-on:click.stop.prevent="$emit('selected-group-changed', group)"
|
||||
>
|
||||
<div class="icons">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
class="text-green-500"
|
||||
>
|
||||
<use v-bind:xlink:href="icon('group')" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="list-item-text">
|
||||
{{ group.name }}
|
||||
</div>
|
||||
<div class="badges">
|
||||
<div class="badge default" v-if="group.feed_item_states_count > 0">
|
||||
<span v-if="group.has_new_unread_items">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
class="text-blue-300"
|
||||
>
|
||||
<use v-bind:xlink:href="icon('update')" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ group.feed_item_states_count }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["group"],
|
||||
};
|
||||
</script>
|
||||
229
resources/js/components/GroupsBrowser.vue
Executable file
229
resources/js/components/GroupsBrowser.vue
Executable file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<div class="w-1/2 h-screen flex flex-col">
|
||||
<div class="h-1/2 overflow-auto">
|
||||
<draggable
|
||||
class="list vertical striped spaced items-rounded"
|
||||
v-model="myGroupsSorted"
|
||||
group="myGroups"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
item-key="id"
|
||||
v-bind:force-fallback="true"
|
||||
v-bind:fallback-tolerance="10"
|
||||
handle=".handle"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<groups-browser-item
|
||||
class="select-none"
|
||||
v-bind:group="element"
|
||||
v-bind:movable="true"
|
||||
v-on:selected="selectedGroup = element"
|
||||
v-bind:class="{
|
||||
selected: selectedGroup === element,
|
||||
}"
|
||||
></groups-browser-item>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<group-form
|
||||
class="h-1/2"
|
||||
v-bind:group="selectedGroup"
|
||||
v-on:unselect="selectedGroup = null"
|
||||
v-on:group-created="onGroupCreated"
|
||||
v-on:group-updated="onGroupUpdated"
|
||||
v-on:group-deleted="onGroupDeleted"
|
||||
v-on:invitation-sent="onInvitationSent"
|
||||
v-on:invitation-accepted="onInvitationAccepted"
|
||||
v-on:invitation-declined="onInvitationDeclined"
|
||||
v-on:leave="onLeaveGroup"
|
||||
>
|
||||
</group-form>
|
||||
</div>
|
||||
<div class="w-1/2 bg-gray-50 dark:bg-gray-850">
|
||||
<article>
|
||||
<header>
|
||||
<h1>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="mr-4">
|
||||
{{ __("Public groups") }}
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
v-bind:placeholder="__('Search')"
|
||||
v-model="search"
|
||||
/>
|
||||
</div>
|
||||
</h1>
|
||||
</header>
|
||||
<div class="list vertical striped spaced items-rounded">
|
||||
<groups-browser-item
|
||||
v-for="group in publicGroups"
|
||||
v-bind:group="group"
|
||||
v-bind:key="group.id"
|
||||
v-bind:movable="false"
|
||||
v-on:join="onJoinGroup"
|
||||
></groups-browser-item>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GroupsBrowserItem from "./GroupsBrowser/GroupsBrowserItem";
|
||||
import GroupForm from "./GroupsBrowser/GroupForm";
|
||||
import draggable from "vuedraggable";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
components: { draggable, GroupForm, GroupsBrowserItem },
|
||||
data: function () {
|
||||
return {
|
||||
positions: [],
|
||||
selectedGroup: null,
|
||||
publicGroups: [],
|
||||
search: "",
|
||||
drag: false,
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
const self = this;
|
||||
|
||||
self.indexMyGroups();
|
||||
self.loadPublicGroups();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
myGroups: "groups/groups",
|
||||
}),
|
||||
myGroupsSorted: {
|
||||
get() {
|
||||
return collect(this.myGroups).sortBy("pivot.position").all();
|
||||
},
|
||||
set(value) {
|
||||
const self = this;
|
||||
const collection = collect(value);
|
||||
let positions = {};
|
||||
|
||||
collection.each(function (group, index) {
|
||||
positions[group.id] = index;
|
||||
});
|
||||
|
||||
self.updatePositions({ positions: positions });
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search: function (value) {
|
||||
this.loadPublicGroups();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
indexMyGroups: "groups/indexMyGroups",
|
||||
createGroup: "groups/createGroup",
|
||||
updateGroup: "groups/updateGroup",
|
||||
updateGroupProperties: "groups/updateProperties",
|
||||
deleteGroup: "groups/deleteGroup",
|
||||
updatePositions: "groups/updatePositions",
|
||||
}),
|
||||
|
||||
loadPublicGroups: function () {
|
||||
const self = this;
|
||||
|
||||
api.get(route("group.index"), { search: self.search }).then(
|
||||
function (response) {
|
||||
self.publicGroups = response.data;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onGroupCreated: function (group) {
|
||||
const self = this;
|
||||
|
||||
self.createGroup(group);
|
||||
},
|
||||
|
||||
onGroupUpdated: function (group) {
|
||||
const self = this;
|
||||
|
||||
self.updateGroup({
|
||||
group: group,
|
||||
newProperties: group,
|
||||
});
|
||||
},
|
||||
|
||||
onGroupDeleted: function (group) {
|
||||
const self = this;
|
||||
|
||||
self.deleteGroup(group).then(function () {
|
||||
self.selectedGroup = null;
|
||||
});
|
||||
},
|
||||
|
||||
onInvitationSent: function (response) {
|
||||
const self = this;
|
||||
|
||||
self.updateGroupProperties({
|
||||
groupId: self.selectedGroup.id,
|
||||
newProperties: response,
|
||||
});
|
||||
},
|
||||
|
||||
onInvitationAccepted: function (group) {
|
||||
const self = this;
|
||||
|
||||
api.post(route("group.accept_invitation", group)).then(function (
|
||||
response
|
||||
) {
|
||||
self.updateGroupProperties({
|
||||
groupId: response.id,
|
||||
newProperties: response,
|
||||
});
|
||||
});
|
||||
|
||||
//TODO: Handle errors
|
||||
},
|
||||
|
||||
onInvitationDeclined: function (group) {
|
||||
const self = this;
|
||||
|
||||
api.post(route("group.reject_invitation", group)).then(function (
|
||||
response
|
||||
) {
|
||||
self.updateGroupProperties({
|
||||
groupId: response.id,
|
||||
newProperties: response,
|
||||
});
|
||||
});
|
||||
|
||||
//TODO: Handle errors
|
||||
},
|
||||
|
||||
onLeaveGroup: function (group) {
|
||||
const self = this;
|
||||
|
||||
api.post(route("group.leave", group)).then(function () {
|
||||
self.indexMyGroups();
|
||||
self.loadPublicGroups();
|
||||
self.selectedGroup = null;
|
||||
});
|
||||
|
||||
//TODO: Handle errors
|
||||
},
|
||||
|
||||
onJoinGroup: function (group) {
|
||||
const self = this;
|
||||
|
||||
api.post(route("group.join", group)).then(function () {
|
||||
self.indexMyGroups();
|
||||
self.loadPublicGroups();
|
||||
self.selectedGroup = null;
|
||||
});
|
||||
|
||||
//TODO: Handle errors
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
257
resources/js/components/GroupsBrowser/GroupForm.vue
Executable file
257
resources/js/components/GroupsBrowser/GroupForm.vue
Executable file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<article>
|
||||
<header>
|
||||
<h1>
|
||||
{{ title }}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div class="body">
|
||||
<div
|
||||
v-if="group && group.description"
|
||||
v-html="group.description"
|
||||
></div>
|
||||
<form
|
||||
v-on:submit.prevent="onSubmit"
|
||||
v-if="
|
||||
!group ||
|
||||
(group &&
|
||||
(group.pivot.status === 'own' ||
|
||||
group.pivot.status === 'created'))
|
||||
"
|
||||
>
|
||||
<input type="hidden" name="id" v-model="id" />
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
v-model="name"
|
||||
v-bind:placeholder="__('Group name')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
v-model="description"
|
||||
v-bind:placeholder="__('Description')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="my-0"
|
||||
><input type="checkbox" v-model="inviteOnly" />
|
||||
<span class="ml-2">{{ __("Invite only") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="my-0"
|
||||
><input type="checkbox" v-model="autoAcceptUsers" />
|
||||
<span class="ml-2">{{ __("Auto-accept users") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-4">
|
||||
<button class="success" type="submit">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use
|
||||
v-bind:xlink:href="icon(!id ? 'add' : 'update')"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{{ !id ? __("Create group") : __("Update group") }}
|
||||
</span>
|
||||
</button>
|
||||
<div v-if="id !== null" class="flex items-center space-x-2">
|
||||
<button
|
||||
class="secondary"
|
||||
v-on:click.stop.prevent="$emit('unselect')"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('cancel')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Cancel") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="group.pivot.status !== 'own'"
|
||||
class="danger"
|
||||
v-on:click.stop.prevent="
|
||||
$emit('group-deleted', group)
|
||||
"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('trash')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Delete") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form
|
||||
v-if="
|
||||
group &&
|
||||
(group.pivot.status === 'own' ||
|
||||
group.pivot.status === 'created')
|
||||
"
|
||||
v-on:submit.prevent="onInviteSubmit"
|
||||
>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
v-model="inviteEmail"
|
||||
v-bind:placeholder="__('E-Mail Address')"
|
||||
/>
|
||||
<button type="submit" class="info">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('join')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Invite in group") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
v-if="group && group.pivot.status === 'invited'"
|
||||
class="mt-4 flex items-center justify-between"
|
||||
>
|
||||
<button
|
||||
class="success"
|
||||
v-on:click="$emit('invitation-accepted', group)"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('check')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Accept invitation") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="danger"
|
||||
v-on:click="$emit('invitation-declined', group)"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('cancel')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Decline invitation") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
group &&
|
||||
(group.pivot.status === 'accepted' ||
|
||||
group.pivot.status === 'joining')
|
||||
"
|
||||
class="mt-4"
|
||||
>
|
||||
<button class="danger" v-on:click="$emit('leave', group)">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('logout')" />
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Leave the group") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["group"],
|
||||
data: function () {
|
||||
return {
|
||||
id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
inviteOnly: false,
|
||||
autoAcceptUsers: false,
|
||||
inviteEmail: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title: function () {
|
||||
if (this.group) {
|
||||
return this.group.name;
|
||||
}
|
||||
|
||||
return this.__("Create group");
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
group: function (group) {
|
||||
if (!group) {
|
||||
this.id = null;
|
||||
this.name = null;
|
||||
this.description = null;
|
||||
this.inviteOnly = false;
|
||||
this.autoAcceptUsers = false;
|
||||
} else {
|
||||
this.id = group.id;
|
||||
this.name = group.name;
|
||||
this.description = group.description;
|
||||
this.inviteOnly = group.invite_only;
|
||||
this.autoAcceptUsers = group.auto_accept_users;
|
||||
}
|
||||
|
||||
this.$forceUpdate();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit: function () {
|
||||
const self = this;
|
||||
const properties = {
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
invite_only: self.inviteOnly,
|
||||
auto_accept_users: self.autoAcceptUsers,
|
||||
};
|
||||
|
||||
if (!properties.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.id) {
|
||||
properties["id"] = self.id;
|
||||
|
||||
self.$emit("group-updated", properties);
|
||||
} else {
|
||||
self.$emit("group-created", properties);
|
||||
|
||||
self.id = null;
|
||||
self.name = null;
|
||||
self.description = null;
|
||||
self.inviteOnly = false;
|
||||
self.autoAcceptUsers = false;
|
||||
}
|
||||
},
|
||||
|
||||
onInviteSubmit: function () {
|
||||
const self = this;
|
||||
|
||||
if (!self.inviteEmail) {
|
||||
return false;
|
||||
}
|
||||
|
||||
api.post(route("group.invite_user", self.group), {
|
||||
email: self.inviteEmail,
|
||||
}).then(function (response) {
|
||||
self.inviteEmail = null;
|
||||
self.$emit("invitation-sent");
|
||||
//TODO: Handle errors
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
121
resources/js/components/GroupsBrowser/GroupsBrowserItem.vue
Executable file
121
resources/js/components/GroupsBrowser/GroupsBrowserItem.vue
Executable file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="list-item">
|
||||
<div class="handle">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('move-v')" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-grow flex justify-between">
|
||||
<div>
|
||||
<h2 class="text-black dark:text-white">{{ group.name }}</h2>
|
||||
<div v-if="group.creator" class="mb-1">
|
||||
{{ __("Created by") }} {{ group.creator.name }}
|
||||
</div>
|
||||
<div class="mb-1">{{ group.description }}</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<span
|
||||
v-if="group.pivot"
|
||||
class="badge"
|
||||
v-bind:class="'group-status-' + group.pivot.status"
|
||||
>{{ status }}</span
|
||||
>
|
||||
<span
|
||||
class="badge default"
|
||||
v-bind:class="{
|
||||
'group-invite-only': group.invite_only,
|
||||
}"
|
||||
v-if="group.invite_only"
|
||||
>{{ __("Invite only") }}</span
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
v-bind:class="{
|
||||
success: group.auto_accept_users,
|
||||
warning: !group.auto_accept_users,
|
||||
}"
|
||||
v-if="!group.invite_only"
|
||||
>{{
|
||||
group.auto_accept_users
|
||||
? __("Auto-accept users")
|
||||
: __("New users must be approved")
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="badges">
|
||||
<span
|
||||
v-if="group.pending_users_count > 0"
|
||||
class="badge warning"
|
||||
v-bind:title="__('Pending users')"
|
||||
><svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('group')" /></svg
|
||||
>{{ group.pending_users_count }}</span
|
||||
>
|
||||
<span class="badge success" v-bind:title="__('Active users')"
|
||||
><svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('group')" /></svg
|
||||
>{{ group.active_users_count }}</span
|
||||
>
|
||||
<button
|
||||
class="info"
|
||||
v-if="group.pivot"
|
||||
v-on:click="$emit('selected', group)"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('update')" />
|
||||
</svg>
|
||||
<span>{{ __("Edit") }} </span>
|
||||
</button>
|
||||
<button
|
||||
class="success"
|
||||
v-if="!group.pivot"
|
||||
v-on:click="$emit('join', group)"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('join')" />
|
||||
</svg>
|
||||
<span
|
||||
>{{ group.auto_accept_users ? __("Join") : __("Apply") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["group", "movable"],
|
||||
computed: {
|
||||
status: function () {
|
||||
const self = this;
|
||||
|
||||
if (!self.group.pivot) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (self.group.pivot.status) {
|
||||
case "own":
|
||||
return self.__("My own group");
|
||||
case "created":
|
||||
return self.__("Created by me");
|
||||
case "invited":
|
||||
return self.__("Invited to join");
|
||||
case "accepted":
|
||||
return self.__("Accepted");
|
||||
case "rejected":
|
||||
return self.__("Rejected");
|
||||
case "left":
|
||||
return self.__("Group left");
|
||||
case "joining":
|
||||
return self.__("Waiting for joining");
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
select: function () {
|
||||
this.$emit("selected");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
285
resources/js/components/Highlights.vue
Executable file
285
resources/js/components/Highlights.vue
Executable file
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<div class="w-1/2 h-screen flex flex-col">
|
||||
<div class="h-2/3 overflow-auto">
|
||||
<draggable
|
||||
class="list vertical striped spaced items-rounded"
|
||||
v-model="sortedHighlights"
|
||||
group="myHighlights"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
item-key="id"
|
||||
v-bind:force-fallback="true"
|
||||
v-bind:fallback-tolerance="10"
|
||||
handle=".handle"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div
|
||||
class="list-item select-none"
|
||||
v-bind:class="{
|
||||
selected:
|
||||
selectedHighlight &&
|
||||
selectedHighlight.id === element.id,
|
||||
}"
|
||||
>
|
||||
<div class="handle">
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use v-bind:xlink:href="icon('move-v')" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icons">
|
||||
<div
|
||||
class="badge font-mono"
|
||||
v-html="highlightText(element.color)"
|
||||
></div>
|
||||
</div>
|
||||
<div class="list-item-text">
|
||||
{{ element.expression }}
|
||||
</div>
|
||||
<div class="tools">
|
||||
<button
|
||||
class="info"
|
||||
v-on:click="selectedHighlight = element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<use
|
||||
v-bind:xlink:href="icon('update')"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ __("Edit") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="h-1/3">
|
||||
<article>
|
||||
<header>
|
||||
<h1>
|
||||
{{
|
||||
selectedHighlight
|
||||
? selectedHighlight.expression
|
||||
: __("Create highlight")
|
||||
}}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="body">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-none mr-2">
|
||||
<div class="form-group">
|
||||
<label for="color">{{ __("Color") }}</label>
|
||||
<input
|
||||
type="color"
|
||||
id="color"
|
||||
v-model="newColor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="form-group">
|
||||
<label for="expression">{{
|
||||
__("Expression")
|
||||
}}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="expression"
|
||||
v-model="newExpression"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
type="submit"
|
||||
class="success"
|
||||
v-on:click="saveHighlight"
|
||||
>
|
||||
<svg fill="currentColor" width="16" height="16">
|
||||
<use
|
||||
v-bind:xlink:href="
|
||||
icon(
|
||||
!selectedHighlight
|
||||
? 'add'
|
||||
: 'update'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Save") }}
|
||||
</span>
|
||||
</button>
|
||||
<div class="flex items-center space-x-1">
|
||||
<button
|
||||
class="secondary"
|
||||
v-on:click="resetForm"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<use
|
||||
v-bind:xlink:href="icon('cancel')"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Cancel") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="danger"
|
||||
v-on:click="onDestroy"
|
||||
v-if="selectedHighlight"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<use
|
||||
v-bind:xlink:href="icon('trash')"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{{ __("Delete") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 bg-gray-50 dark:bg-gray-850"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
import TEXTColor from "textcolor";
|
||||
|
||||
export default {
|
||||
components: { draggable },
|
||||
data: function () {
|
||||
return {
|
||||
positions: [],
|
||||
highlights: highlights,
|
||||
newExpression: null,
|
||||
newColor: "#000000",
|
||||
selectedHighlight: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedHighlights: {
|
||||
get() {
|
||||
return collect(this.highlights).sortBy("position").all();
|
||||
},
|
||||
set(value) {
|
||||
const self = this;
|
||||
const collection = collect(value);
|
||||
let positions = {};
|
||||
|
||||
collection.each(function (highlight, index) {
|
||||
positions[highlight.id] = index;
|
||||
});
|
||||
|
||||
self.updatePositions({ positions: positions });
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedHighlight: function (highlight) {
|
||||
const self = this;
|
||||
|
||||
if (highlight) {
|
||||
self.newExpression = highlight.expression;
|
||||
self.newColor = highlight.color;
|
||||
} else {
|
||||
self.resetForm();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetForm: function () {
|
||||
const self = this;
|
||||
|
||||
self.selectedHighlight = null;
|
||||
self.newExpression = null;
|
||||
self.newColor = "#000000";
|
||||
},
|
||||
saveHighlight: function () {
|
||||
const self = this;
|
||||
|
||||
if (!self.newExpression) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.selectedHighlight) {
|
||||
api.put(route("highlight.update", self.selectedHighlight), {
|
||||
expression: self.newExpression,
|
||||
color: self.newColor,
|
||||
}).then(function (response) {
|
||||
self.highlights = response;
|
||||
self.resetForm();
|
||||
});
|
||||
} else {
|
||||
api.post(route("highlight.store"), {
|
||||
expression: self.newExpression,
|
||||
color: self.newColor,
|
||||
}).then(function (response) {
|
||||
self.highlights = response;
|
||||
self.resetForm();
|
||||
});
|
||||
}
|
||||
},
|
||||
onDestroy: function () {
|
||||
const self = this;
|
||||
api.delete(route("highlight.destroy", self.selectedHighlight)).then(
|
||||
function (response) {
|
||||
self.highlights = response;
|
||||
self.selectedHighlight = null;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
updatePositions: function ({ positions }) {
|
||||
const self = this;
|
||||
|
||||
for (var id in positions) {
|
||||
const highlight = self.highlights.find((h) => h.id == id);
|
||||
|
||||
if (!highlight) {
|
||||
console.warn("Highlight #" + id + " not found");
|
||||
return;
|
||||
}
|
||||
|
||||
highlight.position = positions[id];
|
||||
}
|
||||
|
||||
api.post(route("highlight.update_positions"), {
|
||||
positions: positions,
|
||||
});
|
||||
},
|
||||
|
||||
highlightText: function (color) {
|
||||
let textColor = TEXTColor.findTextColor(color);
|
||||
color =
|
||||
'<span class="highlight" style="color: ' +
|
||||
textColor +
|
||||
"; background-color: " +
|
||||
color +
|
||||
'">' +
|
||||
color +
|
||||
"</span>";
|
||||
|
||||
return color;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
90
resources/js/components/Importer.vue
Executable file
90
resources/js/components/Importer.vue
Executable file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label for="importer">{{ __("Import from") }}:</label>
|
||||
<select v-model="importer" id="importer">
|
||||
<option
|
||||
v-for="(importerData, importerName) in availableImporters"
|
||||
v-bind:key="importerName"
|
||||
v-bind:value="importerName"
|
||||
>
|
||||
{{ importerData["name"] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<component
|
||||
v-bind:is="availableImporters[importer]['view']"
|
||||
v-on:import="onImport"
|
||||
></component>
|
||||
|
||||
<div v-if="uploading" class="mt-2">
|
||||
{{ __("Importing, please wait...") }}
|
||||
</div>
|
||||
<div v-if="uploaded" class="alert success">
|
||||
{{ __("Data imported") }}
|
||||
</div>
|
||||
<div v-if="error" class="alert error">
|
||||
{{ __("A problem occurred, interrupting importation") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImportFromCyca from "./Importers/ImportFromCyca";
|
||||
|
||||
export default {
|
||||
components: { ImportFromCyca },
|
||||
props: ["availableImporters"],
|
||||
data: function () {
|
||||
return {
|
||||
importer: "cyca",
|
||||
uploading: false,
|
||||
uploaded: false,
|
||||
error: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onImport: function (formData) {
|
||||
const self = this;
|
||||
|
||||
if (self.uploading) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.uploaded = false;
|
||||
self.uploading = true;
|
||||
self.error = false;
|
||||
|
||||
formData.append("importer", self.importer);
|
||||
|
||||
fetch(route("account.import"), {
|
||||
body: formData,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRF-TOKEN": document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
.getAttribute("content"),
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
self.uploaded = false;
|
||||
self.error = true;
|
||||
} else {
|
||||
self.uploaded = true;
|
||||
self.error = false;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
self.uploaded = false;
|
||||
self.error = true;
|
||||
})
|
||||
.finally(() => {
|
||||
self.uploading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
30
resources/js/components/Importers/ImportFromCyca.vue
Executable file
30
resources/js/components/Importers/ImportFromCyca.vue
Executable file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label for="file">{{ __("File to import") }}</label>
|
||||
|
||||
<input type="file" id="file" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" v-on:click="onImport">
|
||||
→ {{ __("Import") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
onImport: function () {
|
||||
const input = document.getElementById("file");
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append("file", input.files[0]);
|
||||
|
||||
this.$emit("import", formData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
45
resources/js/components/MetaDataBrowser.vue
Executable file
45
resources/js/components/MetaDataBrowser.vue
Executable file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(value, item) in metaData" v-bind:key="item">
|
||||
<stateful-details
|
||||
class="list-item"
|
||||
v-if="isArrayOrObject(value)"
|
||||
v-bind:name="getStatefulDetailsName(item)"
|
||||
>
|
||||
<summary>{{ item }}</summary>
|
||||
<browser
|
||||
class="vertical list striped items-rounded compact alt"
|
||||
v-bind:meta-data="value"
|
||||
v-bind:parent-name="getStatefulDetailsName(item)"
|
||||
></browser>
|
||||
</stateful-details>
|
||||
<div class="list-item" v-if="!isArrayOrObject(value)">
|
||||
<div class="list-item-title">
|
||||
{{ item }}
|
||||
</div>
|
||||
<div class="list-item-value">
|
||||
<code>{{ value }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StatefulDetails from "./StatefulDetails.vue";
|
||||
export default {
|
||||
components: { StatefulDetails },
|
||||
name: "browser",
|
||||
props: ["metaData", "parentName"],
|
||||
methods: {
|
||||
getStatefulDetailsName: function (name) {
|
||||
const self = this;
|
||||
|
||||
return collect(["details", self.parentName, name]).join("_");
|
||||
},
|
||||
isArrayOrObject: function (variable) {
|
||||
return typeof variable === "array" || typeof variable === "object";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
23
resources/js/components/StatefulDetails.vue
Executable file
23
resources/js/components/StatefulDetails.vue
Executable file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<details v-bind:open="opened" v-on:toggle="onToggle"><slot></slot></details>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["name"],
|
||||
computed: {
|
||||
opened: function () {
|
||||
return localStorage.getItem(this.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onToggle: function (event) {
|
||||
if (event.target.open) {
|
||||
localStorage.setItem(this.name, event.target.open);
|
||||
} else {
|
||||
localStorage.removeItem(this.name);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
24
resources/js/groups.js
vendored
Executable file
24
resources/js/groups.js
vendored
Executable file
@@ -0,0 +1,24 @@
|
||||
require("./modules/bootstrap");
|
||||
|
||||
import { createApp } from "vue";
|
||||
import { createStore } from "vuex";
|
||||
import mixins from "./mixins";
|
||||
import groups from "./store/modules/groups";
|
||||
import GroupsBrowser from "./components/GroupsBrowser.vue";
|
||||
|
||||
const debug = process.env.NODE_ENV !== "production";
|
||||
|
||||
const store = createStore({
|
||||
modules: {
|
||||
groups,
|
||||
},
|
||||
strict: debug,
|
||||
});
|
||||
|
||||
createApp({
|
||||
components: { GroupsBrowser },
|
||||
el: "#app",
|
||||
})
|
||||
.mixin(mixins)
|
||||
.use(store)
|
||||
.mount("#app");
|
||||
12
resources/js/highlights.js
vendored
Executable file
12
resources/js/highlights.js
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
require("./modules/bootstrap");
|
||||
|
||||
import { createApp } from "vue";
|
||||
import mixins from "./mixins";
|
||||
import Highlights from "./components/Highlights.vue";
|
||||
|
||||
createApp({
|
||||
components: { Highlights },
|
||||
el: "#app",
|
||||
})
|
||||
.mixin(mixins)
|
||||
.mount("#app");
|
||||
12
resources/js/import.js
vendored
Executable file
12
resources/js/import.js
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
require("./modules/bootstrap");
|
||||
|
||||
import { createApp } from "vue";
|
||||
import mixins from "./mixins";
|
||||
import Importer from "./components/Importer.vue";
|
||||
|
||||
createApp({
|
||||
components: { Importer },
|
||||
el: "#app",
|
||||
})
|
||||
.mixin(mixins)
|
||||
.mount("#app");
|
||||
35
resources/js/mixins/index.js
vendored
Executable file
35
resources/js/mixins/index.js
vendored
Executable file
@@ -0,0 +1,35 @@
|
||||
import router from "../modules/router";
|
||||
|
||||
window.route = router;
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
route: (name, params) => route(name, params),
|
||||
/**
|
||||
* Return url to an icon
|
||||
*/
|
||||
icon: (name) => {
|
||||
const iconsFileUrl = document
|
||||
.querySelector('meta[name="icons-file-url"]')
|
||||
.getAttribute("content");
|
||||
|
||||
const url = iconsFileUrl + "#" + name;
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
/**
|
||||
* Translate specified string
|
||||
* @param {*} langString
|
||||
*/
|
||||
__: function (langString) {
|
||||
const translation = lang[langString];
|
||||
|
||||
if (translation) {
|
||||
return translation;
|
||||
}
|
||||
|
||||
return langString;
|
||||
},
|
||||
},
|
||||
};
|
||||
98
resources/js/modules/api.js
vendored
Executable file
98
resources/js/modules/api.js
vendored
Executable file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Default headers to apply to every query
|
||||
*/
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRF-TOKEN": document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
.getAttribute("content")
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Send a request and return resulting JSON
|
||||
*/
|
||||
async send(url, params, method = "POST", upload = false) {
|
||||
let headers = defaultHeaders;
|
||||
|
||||
if (upload) {
|
||||
headers["Content-Type"] = "multipart/form-data";
|
||||
}
|
||||
|
||||
let options = {
|
||||
method: method,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
if (params) {
|
||||
if (upload) {
|
||||
options.body = params;
|
||||
} else if (method === "GET") {
|
||||
// Build the query string
|
||||
var query = Object.keys(params)
|
||||
.map(
|
||||
k => {
|
||||
let key = encodeURIComponent(k);
|
||||
let val = params[k];
|
||||
|
||||
if (val.constructor === Array) {
|
||||
let arr = [];
|
||||
|
||||
val.forEach((v => {
|
||||
arr.push(key + "[]=" + encodeURIComponent(v));
|
||||
}));
|
||||
|
||||
return arr.join('&');
|
||||
} else {
|
||||
return key + "=" + encodeURIComponent(val);
|
||||
}
|
||||
}
|
||||
)
|
||||
.join("&");
|
||||
|
||||
url = url + "?" + query;
|
||||
} else {
|
||||
options.body = JSON.stringify(params);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
try {
|
||||
const json = await response.json();
|
||||
|
||||
return json;
|
||||
} catch {
|
||||
return response;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a GET request and return resulting JSON
|
||||
*/
|
||||
async get(url, params) {
|
||||
return this.send(url, params, "GET");
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a POST request and return resulting JSON
|
||||
*/
|
||||
async post(url, params, upload = false) {
|
||||
return this.send(url, params, "POST", upload);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a PUT request and return resulting JSON
|
||||
*/
|
||||
async put(url, params) {
|
||||
return this.send(url, params, "PUT");
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a DELETE request and return resulting JSON
|
||||
*/
|
||||
async delete(url, params) {
|
||||
return this.send(url, params, "DELETE");
|
||||
}
|
||||
};
|
||||
5
resources/js/modules/bootstrap.js
vendored
Executable file
5
resources/js/modules/bootstrap.js
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
//window.Vue = require('vue');
|
||||
window.collect = require("collect.js");
|
||||
|
||||
import api from "./api";
|
||||
window.api = api;
|
||||
37
resources/js/modules/components.js
vendored
Executable file
37
resources/js/modules/components.js
vendored
Executable file
@@ -0,0 +1,37 @@
|
||||
const sets = {
|
||||
app: [
|
||||
"Details/DetailsDocument",
|
||||
"Details/DetailsDocuments",
|
||||
"Details/DetailsFeedItem",
|
||||
"Details/DetailsFolder",
|
||||
"DocumentsList",
|
||||
"FeedItemsList",
|
||||
"FoldersTree",
|
||||
],
|
||||
import: ["Importer"],
|
||||
highlights: ["Highlights", "Highlight"],
|
||||
historyBrowser: ["HistoryBrowser"],
|
||||
groups: [
|
||||
"GroupsBrowser"
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = function(set) {
|
||||
const files = sets[set];
|
||||
|
||||
files.map(file => {
|
||||
const componentName = file
|
||||
.split("/")
|
||||
.pop()
|
||||
.match(
|
||||
/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g
|
||||
)
|
||||
.map(x => x.toLowerCase())
|
||||
.join("-");
|
||||
|
||||
Vue.component(
|
||||
componentName,
|
||||
require("../components/" + file + ".vue").default
|
||||
);
|
||||
});
|
||||
};
|
||||
32
resources/js/modules/router.js
vendored
Executable file
32
resources/js/modules/router.js
vendored
Executable file
@@ -0,0 +1,32 @@
|
||||
const routes = require("./routes.json");
|
||||
|
||||
module.exports = function(name, params) {
|
||||
if (routes[name] === undefined) {
|
||||
console.error("Unknown route ", name);
|
||||
} else {
|
||||
return (
|
||||
document.querySelector("base").getAttribute("href") +
|
||||
"/" +
|
||||
routes[name]
|
||||
.split("/")
|
||||
.map(s => {
|
||||
if (s[0] == "{") {
|
||||
var paramName = s.substring(1, s.length - 1);
|
||||
|
||||
if(params[paramName]) {
|
||||
return params[paramName];
|
||||
}
|
||||
|
||||
if (params.id) {
|
||||
return params.id;
|
||||
}
|
||||
|
||||
return paramName;
|
||||
}
|
||||
|
||||
return s;
|
||||
})
|
||||
.join("/")
|
||||
);
|
||||
}
|
||||
};
|
||||
1
resources/js/modules/routes.json
Executable file
1
resources/js/modules/routes.json
Executable file
@@ -0,0 +1 @@
|
||||
{"logout":"logout","account":"account","home":"\/","account.password":"account\/password","account.import.form":"account\/import","account.import":"account\/import","account.export":"account\/export","group.index_active":"group\/active","group.my_groups":"group\/my_groups","group.update_positions":"group\/update_positions","group.invite_user":"group\/{group}\/invite","group.leave":"group\/{group}\/leave","group.join":"group\/{group}\/join","group.accept_invitation":"group\/{group}\/accept_invitation","group.reject_invitation":"group\/{group}\/reject_invitation","group.index":"group","group.store":"group","group.show":"group\/{group}","group.update":"group\/{group}","group.destroy":"group\/{group}","folder.toggle_branch":"folder\/{folder}\/toggle_branch","folder.details":"folder\/{folder}\/details","folder.set_permission":"folder\/{folder}\/set_permission","folder.per_user_permissions":"folder\/{folder}\/per_user_permissions","folder.users_without_permissions":"folder\/{folder}\/users_without_permissions","folder.remove_permissions":"folder\/{folder}\/remove_permissions\/{user}","folder.index":"folder","folder.store":"folder","folder.show":"folder\/{folder}","folder.update":"folder\/{folder}","folder.destroy":"folder\/{folder}","document.move":"document\/move\/{sourceFolder}\/{targetFolder}","document.destroy_bookmarks":"document\/delete_bookmarks\/{folder}","document.visit":"document\/{document}\/visit","document.store":"document","document.show":"document\/{document}","feed.ignore":"feed\/{feed}\/ignore","feed.follow":"feed\/{feed}\/follow","feed_item.mark_as_read":"feed_item\/mark_as_read","feed_item.index":"feed_item","feed_item.show":"feed_item\/{feed_item}","highlight.update_positions":"highlight\/update_positions","highlight.store":"highlight","highlight.update":"highlight\/{highlight}","highlight.destroy":"highlight\/{highlight}"}
|
||||
13
resources/js/modules/websockets.js
vendored
Executable file
13
resources/js/modules/websockets.js
vendored
Executable file
@@ -0,0 +1,13 @@
|
||||
window.Pusher = require('pusher-js');
|
||||
|
||||
import Echo from "laravel-echo"
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'pusher',
|
||||
key: 'cyca',
|
||||
cluster: 'mt1',
|
||||
forceTLS: false,
|
||||
disableStats: true,
|
||||
wsHost: window.location.hostname,
|
||||
wsPort: 6001
|
||||
});
|
||||
17
resources/js/store/index.js
vendored
Executable file
17
resources/js/store/index.js
vendored
Executable file
@@ -0,0 +1,17 @@
|
||||
import { createStore } from "vuex";
|
||||
import folders from "./modules/folders";
|
||||
import documents from "./modules/documents";
|
||||
import feedItems from "./modules/feeditems";
|
||||
import groups from "./modules/groups";
|
||||
|
||||
const debug = process.env.NODE_ENV !== "production";
|
||||
|
||||
export default createStore({
|
||||
modules: {
|
||||
folders,
|
||||
documents,
|
||||
feedItems,
|
||||
groups,
|
||||
},
|
||||
strict: debug,
|
||||
});
|
||||
235
resources/js/store/modules/documents/actions.js
vendored
Executable file
235
resources/js/store/modules/documents/actions.js
vendored
Executable file
@@ -0,0 +1,235 @@
|
||||
import { collect } from "collect.js";
|
||||
|
||||
/**
|
||||
* Actions on documents
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
async index(
|
||||
{ commit, dispatch, getters },
|
||||
{ documents, updateFeedItems = true }
|
||||
) {
|
||||
commit("setDocuments", documents);
|
||||
|
||||
let selectedDocuments = getters.selectedDocuments;
|
||||
|
||||
if (!selectedDocuments || selectedDocuments.length === 0) {
|
||||
selectedDocuments = documents;
|
||||
}
|
||||
|
||||
if (updateFeedItems) {
|
||||
await dispatch("feedItems/index", selectedDocuments, {
|
||||
root: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
async store({ dispatch }, { url, folder_id, group_id }) {
|
||||
const data = await api.post(route("document.store"), {
|
||||
url: url,
|
||||
folder_id: folder_id,
|
||||
group_id: group_id,
|
||||
});
|
||||
|
||||
dispatch("index", { documents: data });
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark specified documents as selected.
|
||||
*/
|
||||
selectDocuments({ commit, dispatch }, { documents, selectFirstUnread }) {
|
||||
commit("setSelectedDocuments", documents);
|
||||
|
||||
commit("feedItems/setSelectedFeedItems", [], {
|
||||
root: true,
|
||||
});
|
||||
|
||||
dispatch("feedItems/index", documents, { root: true }).then(
|
||||
function () {
|
||||
if (selectFirstUnread && documents) {
|
||||
dispatch("feedItems/selectFirstUnreadFeedItem", null, {
|
||||
root: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Select first document in currently displayed list
|
||||
*/
|
||||
selectFirstDocument({ getters, dispatch }) {
|
||||
const document = collect(getters.documents).first();
|
||||
|
||||
dispatch("selectDocuments", [document]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Select first document containing unread feed items in currently displayed
|
||||
* list
|
||||
*/
|
||||
selectFirstDocumentWithUnreadItems(
|
||||
{ getters, dispatch, rootGetters },
|
||||
{ exclude, selectFirstUnread }
|
||||
) {
|
||||
if (!exclude) {
|
||||
exclude = collect(getters.selectedDocuments).pluck("id").all();
|
||||
}
|
||||
|
||||
const document = collect(getters.documents)
|
||||
.where("feed_item_states_count", ">", 0)
|
||||
.whereNotIn("id", exclude)
|
||||
.first();
|
||||
|
||||
if (document) {
|
||||
dispatch("selectDocuments", {
|
||||
documents: [document],
|
||||
selectFirstUnread: selectFirstUnread,
|
||||
});
|
||||
} else {
|
||||
if (rootGetters["folders/selectedFolder"].type === "unread_items") {
|
||||
dispatch("selectDocuments", {
|
||||
selectFirstUnread: selectFirstUnread,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remember documents being dragged
|
||||
*/
|
||||
startDraggingDocuments({ commit }, documents) {
|
||||
commit("setDraggedDocuments", documents);
|
||||
},
|
||||
|
||||
/**
|
||||
* Forget dragged documents
|
||||
*/
|
||||
stopDraggingDocuments({ commit }) {
|
||||
commit("setDraggedDocuments", []);
|
||||
},
|
||||
|
||||
/**
|
||||
* Move selected documents into specified folder
|
||||
*/
|
||||
async dropIntoFolder(
|
||||
{ getters, commit, dispatch },
|
||||
{ sourceFolder, targetFolder }
|
||||
) {
|
||||
const documents = collect(getters.draggedDocuments).pluck("id").all();
|
||||
|
||||
if (!documents || documents.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await api.post(
|
||||
route("document.move", {
|
||||
sourceFolder: sourceFolder.id,
|
||||
targetFolder: targetFolder.id,
|
||||
}),
|
||||
{
|
||||
documents: documents,
|
||||
}
|
||||
);
|
||||
|
||||
commit("setDraggedDocuments", []);
|
||||
commit("setSelectedDocuments", []);
|
||||
|
||||
const newDocumentsList = collect(getters.documents).whereNotIn(
|
||||
"id",
|
||||
documents
|
||||
);
|
||||
|
||||
dispatch("index", { documents: newDocumentsList });
|
||||
dispatch("feedItems/index", getters.feeds, { root: true });
|
||||
dispatch("feedItems/updateUnreadFeedItemsCount", response, {
|
||||
root: true,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Increment visits for specified document
|
||||
*/
|
||||
async incrementVisits({ commit }, { document }) {
|
||||
const data = await api.post(
|
||||
route("document.visit", { document: document.id })
|
||||
);
|
||||
|
||||
commit("update", {
|
||||
document: document,
|
||||
newProperties: data,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Open specified document in background
|
||||
*/
|
||||
openDocument({ dispatch }, { document }) {
|
||||
window.open(document.url);
|
||||
|
||||
dispatch("incrementVisits", { document: document });
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove specified documents from specified folder
|
||||
*/
|
||||
async destroy({ commit, getters, dispatch }, { folder, documents }) {
|
||||
commit("setSelectedDocuments", []);
|
||||
|
||||
const data = await api.post(
|
||||
route("document.destroy_bookmarks", { folder: folder.id }),
|
||||
{
|
||||
documents: collect(documents).pluck("id").all(),
|
||||
}
|
||||
);
|
||||
|
||||
dispatch("index", { documents: data });
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
update({ commit, getters }, { documentId, newProperties }) {
|
||||
const document = getters.documents.find((d) => d.id == documentId);
|
||||
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
commit("update", { document: document, newProperties: newProperties });
|
||||
},
|
||||
|
||||
/**
|
||||
* Load every data available for specified document, unless it was already
|
||||
* loaded
|
||||
*/
|
||||
load({ dispatch }, document) {
|
||||
if (!document.loaded) {
|
||||
return api
|
||||
.get(route("document.show", document))
|
||||
.then(function (response) {
|
||||
response.loaded = true;
|
||||
|
||||
return dispatch("update", {
|
||||
documentId: document.id,
|
||||
newProperties: response,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
followFeed({ commit }, feed) {
|
||||
api.post(route("feed.follow", feed));
|
||||
commit("ignoreFeed", { feed: feed, ignored: false });
|
||||
},
|
||||
|
||||
ignoreFeed({ commit }, feed) {
|
||||
api.post(route("feed.ignore", feed));
|
||||
commit("ignoreFeed", { feed: feed, ignored: true });
|
||||
},
|
||||
};
|
||||
29
resources/js/store/modules/documents/getters.js
vendored
Executable file
29
resources/js/store/modules/documents/getters.js
vendored
Executable file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Documents getters
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Return documents list
|
||||
*/
|
||||
documents: state => {
|
||||
return collect(state.documents).sortBy('title').all();
|
||||
},
|
||||
/**
|
||||
* Return currently selected documents
|
||||
*/
|
||||
selectedDocuments: state => {
|
||||
return state.selectedDocuments;
|
||||
},
|
||||
/**
|
||||
* Return the first selected document
|
||||
*/
|
||||
selectedDocument: state => {
|
||||
return collect(state.selectedDocuments).first();
|
||||
},
|
||||
/**
|
||||
* Return documents being dragged
|
||||
*/
|
||||
draggedDocuments: state => {
|
||||
return state.draggedDocuments;
|
||||
}
|
||||
}
|
||||
12
resources/js/store/modules/documents/index.js
vendored
Executable file
12
resources/js/store/modules/documents/index.js
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
import state from "./state";
|
||||
import getters from "./getters";
|
||||
import actions from "./actions";
|
||||
import mutations from "./mutations";
|
||||
|
||||
export default {
|
||||
'namespaced': true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
46
resources/js/store/modules/documents/mutations.js
vendored
Executable file
46
resources/js/store/modules/documents/mutations.js
vendored
Executable file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Documents mutations
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Store documents list
|
||||
* @param {*} state
|
||||
* @param {*} documents
|
||||
*/
|
||||
setDocuments(state, documents) {
|
||||
state.documents = documents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark specified documents as selected
|
||||
* @param {*} state
|
||||
* @param {*} documents
|
||||
*/
|
||||
setSelectedDocuments(state, documents) {
|
||||
state.selectedDocuments = documents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remember documents being dragged
|
||||
* @param {*} state
|
||||
* @param {*} documents
|
||||
*/
|
||||
setDraggedDocuments(state, documents) {
|
||||
state.draggedDocuments = documents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update document's properties
|
||||
* @param {*} state
|
||||
* @param {*} param1
|
||||
*/
|
||||
update(state, {document, newProperties}) {
|
||||
for(var property in newProperties) {
|
||||
document[property] = newProperties[property];
|
||||
}
|
||||
},
|
||||
|
||||
ignoreFeed(state, {feed, ignored}) {
|
||||
feed.is_ignored = ignored;
|
||||
}
|
||||
}
|
||||
14
resources/js/store/modules/documents/state.js
vendored
Executable file
14
resources/js/store/modules/documents/state.js
vendored
Executable file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
/**
|
||||
* Documents list
|
||||
*/
|
||||
documents: [],
|
||||
/**
|
||||
* Selected documents
|
||||
*/
|
||||
selectedDocuments: [],
|
||||
/**
|
||||
* Documents being dragged
|
||||
*/
|
||||
draggedDocuments: []
|
||||
}
|
||||
228
resources/js/store/modules/feeditems/actions.js
vendored
Executable file
228
resources/js/store/modules/feeditems/actions.js
vendored
Executable file
@@ -0,0 +1,228 @@
|
||||
import { collect } from "collect.js";
|
||||
|
||||
/**
|
||||
* Feed items actions
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Load feed items for specified feeds
|
||||
*/
|
||||
async index({ getters, commit }, documents) {
|
||||
const feeds = collect(documents)
|
||||
.pluck("feeds")
|
||||
.flatten(1)
|
||||
.pluck("id")
|
||||
.all();
|
||||
|
||||
let nextPage = 0;
|
||||
let feedItems = [];
|
||||
const currentlySelectedItem = getters.selectedFeedItem;
|
||||
|
||||
if (feeds.length > 0) {
|
||||
const data = await api.get(route("feed_item.index"), {
|
||||
feeds: feeds,
|
||||
});
|
||||
|
||||
feedItems = data.data;
|
||||
nextPage = data.next_page_url !== null ? data.current_page + 1 : 0;
|
||||
}
|
||||
|
||||
if (currentlySelectedItem) {
|
||||
const itemFeeds = collect(currentlySelectedItem.feeds)
|
||||
.pluck("id")
|
||||
.flatten(1);
|
||||
|
||||
if (itemFeeds.intersect(feeds)) {
|
||||
let collection = collect(feedItems);
|
||||
|
||||
if (!collection.where("id", currentlySelectedItem.id).first()) {
|
||||
feedItems.push(currentlySelectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commit("setNextPage", nextPage);
|
||||
commit("setFeeds", feeds);
|
||||
commit("setFeedItems", feedItems);
|
||||
},
|
||||
|
||||
/**
|
||||
* Infinite loading
|
||||
*/
|
||||
async loadMoreFeedItems({ getters, commit }) {
|
||||
if (!getters.nextPage || !getters.feeds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = getters.feedItems;
|
||||
const data = await api.get(route("feed_item.index"), {
|
||||
page: getters.nextPage,
|
||||
feeds: getters.feeds,
|
||||
});
|
||||
|
||||
const newItems = [...items, ...data.data];
|
||||
|
||||
commit(
|
||||
"setNextPage",
|
||||
data.next_page_url !== null ? data.current_page + 1 : 0
|
||||
);
|
||||
|
||||
commit("setFeeds", getters.feeds);
|
||||
commit("setFeedItems", newItems);
|
||||
},
|
||||
|
||||
/**
|
||||
* Change selected feed items
|
||||
*/
|
||||
async selectFeedItems({ commit }, feedItems) {
|
||||
if (feedItems.length === 1) {
|
||||
const data = await api.get(route("feed_item.show", feedItems[0]));
|
||||
commit("update", { feedItem: feedItems[0], newProperties: data });
|
||||
}
|
||||
|
||||
commit("setSelectedFeedItems", feedItems);
|
||||
},
|
||||
|
||||
/**
|
||||
* Select first unread item in current list
|
||||
*/
|
||||
selectFirstUnreadFeedItem({ getters, dispatch }, exclude) {
|
||||
if (!exclude) {
|
||||
exclude = [];
|
||||
}
|
||||
|
||||
const nextFeedItem = collect(getters.feedItems)
|
||||
.where("feed_item_states_count", ">", 0)
|
||||
.whereNotIn("id", exclude)
|
||||
.first();
|
||||
|
||||
if (nextFeedItem) {
|
||||
dispatch("selectFeedItems", [nextFeedItem]);
|
||||
} else {
|
||||
dispatch(
|
||||
"documents/selectFirstDocumentWithUnreadItems",
|
||||
{ selectFirstUnread: true },
|
||||
{
|
||||
root: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
selectNextUnreadFeedItem({ getters, dispatch }, currentId) {
|
||||
if (!currentId) {
|
||||
return dispatch("selectFirstUnreadFeedItem");
|
||||
}
|
||||
|
||||
const nextFeedItem = collect(getters.feedItems)
|
||||
.where("feed_item_states_count", ">", 0)
|
||||
.skipUntil((item) => item.id === currentId)
|
||||
.skip(1)
|
||||
.shift();
|
||||
|
||||
if (nextFeedItem) {
|
||||
dispatch("selectFeedItems", [nextFeedItem]);
|
||||
} else {
|
||||
dispatch(
|
||||
"documents/selectFirstDocumentWithUnreadItems",
|
||||
{ selectFirstUnread: true },
|
||||
{
|
||||
root: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark feed items as read
|
||||
*/
|
||||
markAsRead({ dispatch, commit, getters }, data) {
|
||||
const nextPage = getters.nextPage;
|
||||
|
||||
// Avoid unwanted reloading
|
||||
commit("setNextPage", null);
|
||||
|
||||
if ("feed_items" in data) {
|
||||
dispatch("selectNextUnreadFeedItem", data.feed_items[0]);
|
||||
} else if ("documents" in data) {
|
||||
dispatch(
|
||||
"documents/selectFirstDocumentWithUnreadItems",
|
||||
{ exclude: data.documents },
|
||||
{ root: true }
|
||||
);
|
||||
}
|
||||
|
||||
api.post(route("feed_item.mark_as_read"), data).then(function (
|
||||
response
|
||||
) {
|
||||
dispatch("updateUnreadFeedItemsCount", response);
|
||||
commit("setNextPage", nextPage);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Change number of unread feed items everywhere it's necessary
|
||||
*/
|
||||
updateUnreadFeedItemsCount({ commit, dispatch, getters }, data) {
|
||||
if ("updated_feed_items" in data && data.updated_feed_items !== null) {
|
||||
const feedItems = collect(getters.feedItems).whereIn(
|
||||
"id",
|
||||
data.updated_feed_items
|
||||
);
|
||||
|
||||
feedItems.each(function (feedItem) {
|
||||
commit("update", {
|
||||
feedItem: feedItem,
|
||||
newProperties: {
|
||||
feed_item_states_count: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ("documents" in data) {
|
||||
for (var documentId in data.documents) {
|
||||
dispatch(
|
||||
"documents/update",
|
||||
{
|
||||
documentId: documentId,
|
||||
newProperties: {
|
||||
feed_item_states_count: data.documents[documentId],
|
||||
},
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ("folders" in data) {
|
||||
for (var folderId in data.folders) {
|
||||
dispatch(
|
||||
"folders/updateProperties",
|
||||
{
|
||||
folderId: folderId,
|
||||
newProperties: {
|
||||
feed_item_states_count: data.folders[folderId],
|
||||
},
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ("groups" in data) {
|
||||
for (var groupId in data.groups) {
|
||||
dispatch(
|
||||
"groups/updateProperties",
|
||||
{
|
||||
groupId: groupId,
|
||||
newProperties: {
|
||||
feed_item_states_count: data.groups[groupId],
|
||||
},
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
48
resources/js/store/modules/feeditems/getters.js
vendored
Executable file
48
resources/js/store/modules/feeditems/getters.js
vendored
Executable file
@@ -0,0 +1,48 @@
|
||||
export default {
|
||||
/**
|
||||
* Feed items present in current selection (folder, document, feed)
|
||||
*/
|
||||
feedItems: state => {
|
||||
return state.feedItems;
|
||||
},
|
||||
/**
|
||||
* Return first feed item in current list
|
||||
*/
|
||||
feedItem: state => {
|
||||
return collect(state.feedItems).first();
|
||||
},
|
||||
/**
|
||||
* Return currently selected feed items
|
||||
*/
|
||||
selectedFeedItems: state => {
|
||||
if(!state.selectedFeedItems) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return state.selectedFeedItems;
|
||||
},
|
||||
/**
|
||||
* Return the first selected feed item
|
||||
*/
|
||||
selectedFeedItem: state => {
|
||||
return collect(state.selectedFeedItems).first();
|
||||
},
|
||||
/**
|
||||
* Return next page #
|
||||
*/
|
||||
nextPage: state => {
|
||||
return state.nextPage;
|
||||
},
|
||||
/**
|
||||
* Return feeds associated with current feed items
|
||||
*/
|
||||
feeds: state => {
|
||||
return state.feeds;
|
||||
},
|
||||
/**
|
||||
* Return a boolean value indicating if we can load more feed items
|
||||
*/
|
||||
canLoadMore: state => {
|
||||
return state.nextPage > 1;
|
||||
}
|
||||
};
|
||||
12
resources/js/store/modules/feeditems/index.js
vendored
Executable file
12
resources/js/store/modules/feeditems/index.js
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
import state from "./state";
|
||||
import getters from "./getters";
|
||||
import actions from "./actions";
|
||||
import mutations from "./mutations";
|
||||
|
||||
export default {
|
||||
'namespaced': true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
52
resources/js/store/modules/feeditems/mutations.js
vendored
Executable file
52
resources/js/store/modules/feeditems/mutations.js
vendored
Executable file
@@ -0,0 +1,52 @@
|
||||
export default {
|
||||
/**
|
||||
* Set feed items list
|
||||
* @param {*} state
|
||||
* @param {*} feedItems
|
||||
*/
|
||||
setFeedItems(state, feedItems) {
|
||||
state.feedItems = feedItems;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set next page #
|
||||
* @param {*} state
|
||||
* @param {*} page
|
||||
*/
|
||||
setNextPage(state, page) {
|
||||
state.nextPage = page;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set feeds associated to current feed items
|
||||
* @param {*} state
|
||||
* @param {*} feeds
|
||||
*/
|
||||
setFeeds(state, feeds) {
|
||||
state.feeds = feeds;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set selected feed items
|
||||
* @param {*} state
|
||||
* @param {*} feedItems
|
||||
*/
|
||||
setSelectedFeedItems(state, feedItems) {
|
||||
if(!feedItems) {
|
||||
feedItems = [];
|
||||
}
|
||||
|
||||
state.selectedFeedItems = feedItems;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update feed item's properties
|
||||
* @param {*} state
|
||||
* @param {*} param1
|
||||
*/
|
||||
update(state, {feedItem, newProperties}) {
|
||||
for(var property in newProperties) {
|
||||
feedItem[property] = newProperties[property];
|
||||
}
|
||||
},
|
||||
};
|
||||
18
resources/js/store/modules/feeditems/state.js
vendored
Executable file
18
resources/js/store/modules/feeditems/state.js
vendored
Executable file
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
/**
|
||||
* Feed items present in current selection (folder, document, feed)
|
||||
*/
|
||||
feedItems: [],
|
||||
/**
|
||||
* Next page #
|
||||
*/
|
||||
nextPage: 0,
|
||||
/**
|
||||
* Feeds associated with current feed items
|
||||
*/
|
||||
feeds: [],
|
||||
/**
|
||||
* Selected feed items
|
||||
*/
|
||||
selectedFeedItems: [],
|
||||
};
|
||||
231
resources/js/store/modules/folders/actions.js
vendored
Executable file
231
resources/js/store/modules/folders/actions.js
vendored
Executable file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Actions on folders
|
||||
*/
|
||||
export default {
|
||||
/*
|
||||
* -------------------------------------------------------------------------
|
||||
* ----| CRUD |-------------------------------------------------------------
|
||||
* -------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
async index({ getters, commit, dispatch }, { folders, show }) {
|
||||
commit("setFolders", folders);
|
||||
|
||||
if (show) {
|
||||
if (show === "unread_items") {
|
||||
show = getters.getUnreadItemsFolder;
|
||||
}
|
||||
|
||||
dispatch("show", { folder: show });
|
||||
} else {
|
||||
dispatch("show", {});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
show(
|
||||
{ commit, getters, dispatch },
|
||||
{ folder, deselectDocuments = true, updateFeedItems = true }
|
||||
) {
|
||||
const currentSelectedFolder = getters.selectedFolder;
|
||||
|
||||
if (!folder) {
|
||||
folder = currentSelectedFolder;
|
||||
} else if (Number.isInteger(folder)) {
|
||||
folder = getters.folders.find((f) => f.id === folder);
|
||||
}
|
||||
|
||||
commit("setSelectedFolder", folder);
|
||||
|
||||
if (deselectDocuments) {
|
||||
dispatch("documents/selectDocuments", [], { root: true });
|
||||
}
|
||||
|
||||
api.get(route("folder.show", folder)).then(function (response) {
|
||||
dispatch(
|
||||
"documents/index",
|
||||
{ documents: response, updateFeedItems: updateFeedItems },
|
||||
{ root: true }
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Load folder's details
|
||||
*/
|
||||
async loadDetails({ dispatch }, folder) {
|
||||
if (!folder.details_loaded && !folder.details_loading) {
|
||||
dispatch("updateProperties", {
|
||||
folderId: folder.id,
|
||||
newProperties: {
|
||||
details_loading: true,
|
||||
},
|
||||
});
|
||||
|
||||
let response = await api.get(route("folder.details", folder));
|
||||
|
||||
response.details_loaded = true;
|
||||
response.details_loading = false;
|
||||
|
||||
dispatch("updateProperties", {
|
||||
folderId: folder.id,
|
||||
newProperties: response,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
store({ dispatch }, { title, parent_id, group_id }) {
|
||||
return api
|
||||
.post(route("folder.store"), {
|
||||
title,
|
||||
parent_id,
|
||||
group_id,
|
||||
})
|
||||
.then((data) => {
|
||||
dispatch("index", { folders: data });
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
async update({ dispatch }, { folder, newProperties }) {
|
||||
const data = await api.put(
|
||||
route("folder.update", folder),
|
||||
newProperties
|
||||
);
|
||||
|
||||
dispatch("updateProperties", {
|
||||
folderId: folder.id,
|
||||
newProperties: data,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
updateProperties({ commit, getters }, { folderId, newProperties }) {
|
||||
const folder = getters.folders.find((f) => f.id == folderId);
|
||||
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
commit("update", { folder: folder, newProperties: newProperties });
|
||||
},
|
||||
|
||||
updatePermission({ dispatch }, { folder, user, ability, granted }) {
|
||||
api.post(route("folder.set_permission", folder), {
|
||||
ability: ability,
|
||||
granted: granted,
|
||||
user_id: user,
|
||||
}).then(function (response) {
|
||||
dispatch("updateProperties", {
|
||||
folderId: folder.id,
|
||||
newProperties: response,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
async destroy({ dispatch }, folder) {
|
||||
const data = await api.delete(route("folder.destroy", folder));
|
||||
|
||||
dispatch("index", { folders: data });
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle specified folder expanded/collapsed
|
||||
*/
|
||||
toggleExpanded({ dispatch }, folder) {
|
||||
dispatch("update", {
|
||||
folder: folder,
|
||||
newProperties: {
|
||||
...folder,
|
||||
...{
|
||||
is_expanded: !folder.is_expanded,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle specified folder's branch expanded/collapsed
|
||||
*/
|
||||
async toggleBranch({ commit }, folder) {
|
||||
const response = await api.post(route("folder.toggle_branch", folder));
|
||||
|
||||
commit("setFolders", response);
|
||||
},
|
||||
|
||||
/*
|
||||
* -------------------------------------------------------------------------
|
||||
* ----| Drag'n'drop |------------------------------------------------------
|
||||
* -------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Remind folder being dragged
|
||||
*/
|
||||
startDraggingFolder({ commit }, folder) {
|
||||
commit("setDraggedFolder", folder);
|
||||
},
|
||||
|
||||
/**
|
||||
* Forget folder being dragged
|
||||
*/
|
||||
stopDraggingFolder({ commit }) {
|
||||
commit("setDraggedFolder", null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Drop something into specified folder
|
||||
*/
|
||||
async dropIntoFolder({ getters, dispatch }, folder) {
|
||||
var sourceFolder = getters.draggedFolder;
|
||||
|
||||
if (!sourceFolder) {
|
||||
sourceFolder = getters.selectedFolder;
|
||||
|
||||
await dispatch(
|
||||
"documents/dropIntoFolder",
|
||||
{ sourceFolder: sourceFolder, targetFolder: folder },
|
||||
{ root: true }
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
sourceFolder.parent_id === folder.id ||
|
||||
sourceFolder.id === folder.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newProperties = {
|
||||
parent_id: folder.id,
|
||||
};
|
||||
|
||||
await dispatch("update", {
|
||||
folder: sourceFolder,
|
||||
newProperties: {
|
||||
...sourceFolder,
|
||||
...newProperties,
|
||||
},
|
||||
}).then(function () {
|
||||
dispatch("groups/show", {}, { root: true });
|
||||
});
|
||||
},
|
||||
};
|
||||
37
resources/js/store/modules/folders/getters.js
vendored
Executable file
37
resources/js/store/modules/folders/getters.js
vendored
Executable file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Folders getters
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Return folders tree
|
||||
*/
|
||||
folders: state => {
|
||||
return state.folders;
|
||||
},
|
||||
/**
|
||||
* Return currently selected folder
|
||||
*/
|
||||
selectedFolder: state => {
|
||||
var folders = state.folders ? state.folders : [];
|
||||
|
||||
return folders.find(folder => folder.is_selected);
|
||||
},
|
||||
/**
|
||||
* Return folder being dragged
|
||||
*/
|
||||
draggedFolder: state => {
|
||||
return state.draggedFolder;
|
||||
},
|
||||
/**
|
||||
* Return a folder by its id
|
||||
*/
|
||||
getFolderById: (state) => (id) => {
|
||||
return state.folders.find(folder => folder.id === id);
|
||||
},
|
||||
/**
|
||||
* Return the "unread items" folder
|
||||
*/
|
||||
getUnreadItemsFolder: (state) => {
|
||||
return state.folders.find(folder => folder.type === 'unread_items');
|
||||
}
|
||||
}
|
||||
12
resources/js/store/modules/folders/index.js
vendored
Executable file
12
resources/js/store/modules/folders/index.js
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
import state from "./state";
|
||||
import getters from "./getters";
|
||||
import actions from "./actions";
|
||||
import mutations from "./mutations";
|
||||
|
||||
export default {
|
||||
'namespaced': true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
52
resources/js/store/modules/folders/mutations.js
vendored
Executable file
52
resources/js/store/modules/folders/mutations.js
vendored
Executable file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Folders mutations
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Store folders list
|
||||
* @param {*} state
|
||||
* @param {*} folders
|
||||
*/
|
||||
setFolders(state, folders) {
|
||||
state.folders = folders;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unselect all folders, and mark specified folder as selected
|
||||
* @param {*} state
|
||||
* @param {*} folder
|
||||
*/
|
||||
setSelectedFolder(state, folder) {
|
||||
state.folders.find((f) => (f.is_selected = false));
|
||||
|
||||
folder.is_selected = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update folder's properties
|
||||
* @param {*} state
|
||||
* @param {*} param1
|
||||
*/
|
||||
update(state, { folder, newProperties }) {
|
||||
for (var property in newProperties) {
|
||||
folder[property] = newProperties[property];
|
||||
}
|
||||
|
||||
if (folder.type === "unread_items") {
|
||||
if (folder.feed_item_states_count) {
|
||||
folder.iconColor = "folder-unread-not-empty";
|
||||
} else {
|
||||
folder.iconColor = "folder-unread";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store folder being dragged during a drag'n'drop process
|
||||
* @param {*} state
|
||||
* @param {*} folder
|
||||
*/
|
||||
setDraggedFolder(state, folder) {
|
||||
state.draggedFolder = folder;
|
||||
},
|
||||
};
|
||||
10
resources/js/store/modules/folders/state.js
vendored
Executable file
10
resources/js/store/modules/folders/state.js
vendored
Executable file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
/**
|
||||
* Folders tree
|
||||
*/
|
||||
folders: [],
|
||||
/**
|
||||
* Folder being dragged
|
||||
*/
|
||||
draggedFolder: null
|
||||
}
|
||||
137
resources/js/store/modules/groups/actions.js
vendored
Executable file
137
resources/js/store/modules/groups/actions.js
vendored
Executable file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Actions on groups
|
||||
*/
|
||||
export default {
|
||||
/*
|
||||
* -------------------------------------------------------------------------
|
||||
* ----| CRUD |-------------------------------------------------------------
|
||||
* -------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Display a listing of the resource - Groups in which user is active
|
||||
*/
|
||||
async indexActive({ commit }) {
|
||||
const data = await api.get(route("group.index_active"));
|
||||
|
||||
commit("setGroups", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display a listing of the resource - Groups associated with this user
|
||||
*/
|
||||
async indexMyGroups({ commit }) {
|
||||
const data = await api.get(route("group.my_groups"));
|
||||
|
||||
commit("setGroups", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
show({ getters, dispatch, rootGetters }, { group, folder }) {
|
||||
const currentSelectedGroup = getters.selectedGroup;
|
||||
|
||||
if (!group) {
|
||||
group = currentSelectedGroup;
|
||||
} else if (Number.isInteger(group)) {
|
||||
group = getters.groups.find((g) => g.id === group);
|
||||
}
|
||||
|
||||
dispatch("selectGroup", group);
|
||||
|
||||
dispatch("documents/selectDocuments", [], { root: true });
|
||||
|
||||
api.get(route("group.show", group)).then(function (folders) {
|
||||
if (folder) {
|
||||
dispatch(
|
||||
"folders/index",
|
||||
{ folders: folders, show: folder },
|
||||
{ root: true }
|
||||
);
|
||||
} else {
|
||||
dispatch("folders/index", { folders: folders }, { root: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark specified group as selected
|
||||
*/
|
||||
selectGroup({ commit, getters }, group) {
|
||||
if (Number.isInteger(group)) {
|
||||
group = getters.groups.find((g) => g.id === group);
|
||||
}
|
||||
|
||||
commit("setSelectedGroup", group);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a group
|
||||
*/
|
||||
updateGroup({ dispatch }, { group, newProperties }) {
|
||||
api.put(route("group.update", group), newProperties).then(function (
|
||||
response
|
||||
) {
|
||||
dispatch("updateProperties", {
|
||||
groupId: group.id,
|
||||
newProperties: response,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete group
|
||||
*/
|
||||
deleteGroup({ commit }, group) {
|
||||
api.delete(route("group.destroy", group)).then(function (response) {
|
||||
commit("setGroups", response);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
updateProperties({ commit, getters }, { groupId, newProperties }) {
|
||||
const group = getters.groups.find((g) => g.id == groupId);
|
||||
|
||||
if (!group) {
|
||||
console.warn("Group #" + groupId + " not found");
|
||||
return;
|
||||
}
|
||||
|
||||
commit("update", { group: group, newProperties: newProperties });
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a group
|
||||
*/
|
||||
createGroup({ commit }, properties) {
|
||||
api.post(route("group.store"), properties).then(function (response) {
|
||||
commit("setGroups", response);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update my groups positions
|
||||
*/
|
||||
updatePositions({ getters, commit }, { positions }) {
|
||||
for (var groupId in positions) {
|
||||
const group = getters.groups.find((g) => g.id == groupId);
|
||||
|
||||
if (!group) {
|
||||
console.warn("Group #" + groupId + " not found");
|
||||
return;
|
||||
}
|
||||
|
||||
commit("updatePosition", {
|
||||
group: group,
|
||||
position: positions[groupId],
|
||||
});
|
||||
}
|
||||
|
||||
api.post(route("group.update_positions"), {
|
||||
positions: positions,
|
||||
});
|
||||
},
|
||||
};
|
||||
19
resources/js/store/modules/groups/getters.js
vendored
Executable file
19
resources/js/store/modules/groups/getters.js
vendored
Executable file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Groups getters
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Return groups
|
||||
*/
|
||||
groups: state => {
|
||||
return state.groups;
|
||||
},
|
||||
/**
|
||||
* Return currently selected group
|
||||
*/
|
||||
selectedGroup: state => {
|
||||
var groups = state.groups ? state.groups : [];
|
||||
|
||||
return groups.find(group => group.is_selected);
|
||||
}
|
||||
}
|
||||
12
resources/js/store/modules/groups/index.js
vendored
Executable file
12
resources/js/store/modules/groups/index.js
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
import state from "./state";
|
||||
import getters from "./getters";
|
||||
import actions from "./actions";
|
||||
import mutations from "./mutations";
|
||||
|
||||
export default {
|
||||
'namespaced': true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
39
resources/js/store/modules/groups/mutations.js
vendored
Executable file
39
resources/js/store/modules/groups/mutations.js
vendored
Executable file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Groups mutations
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Store groups list
|
||||
* @param {*} state
|
||||
* @param {*} groups
|
||||
*/
|
||||
setGroups(state, groups) {
|
||||
state.groups = groups;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unselect all groups, and mark specified group as selected
|
||||
* @param {*} state
|
||||
* @param {*} group
|
||||
*/
|
||||
setSelectedGroup(state, group) {
|
||||
state.groups.find(g => (g.is_selected = false));
|
||||
|
||||
group.is_selected = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update group's properties
|
||||
* @param {*} state
|
||||
* @param {*} param1
|
||||
*/
|
||||
update(state, { group, newProperties }) {
|
||||
for (var property in newProperties) {
|
||||
group[property] = newProperties[property];
|
||||
}
|
||||
},
|
||||
|
||||
updatePosition(state, { group, position }) {
|
||||
group.pivot.position = position;
|
||||
}
|
||||
};
|
||||
6
resources/js/store/modules/groups/state.js
vendored
Executable file
6
resources/js/store/modules/groups/state.js
vendored
Executable file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
/**
|
||||
* Groups
|
||||
*/
|
||||
groups: []
|
||||
}
|
||||
19
resources/lang/en/auth.php
Executable file
19
resources/lang/en/auth.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'These credentials do not match our records.',
|
||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||
|
||||
];
|
||||
19
resources/lang/en/pagination.php
Executable file
19
resources/lang/en/pagination.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Previous',
|
||||
'next' => 'Next »',
|
||||
|
||||
];
|
||||
22
resources/lang/en/passwords.php
Executable file
22
resources/lang/en/passwords.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| has failed, such as for an invalid token or invalid new password.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'Your password has been reset!',
|
||||
'sent' => 'We have emailed your password reset link!',
|
||||
'throttled' => 'Please wait before retrying.',
|
||||
'token' => 'This password reset token is invalid.',
|
||||
'user' => "We can't find a user with that email address.",
|
||||
|
||||
];
|
||||
151
resources/lang/en/validation.php
Executable file
151
resources/lang/en/validation.php
Executable file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'The :attribute must be accepted.',
|
||||
'active_url' => 'The :attribute is not a valid URL.',
|
||||
'after' => 'The :attribute must be a date after :date.',
|
||||
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
|
||||
'alpha' => 'The :attribute may only contain letters.',
|
||||
'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.',
|
||||
'alpha_num' => 'The :attribute may only contain letters and numbers.',
|
||||
'array' => 'The :attribute must be an array.',
|
||||
'before' => 'The :attribute must be a date before :date.',
|
||||
'before_or_equal' => 'The :attribute must be a date before or equal to :date.',
|
||||
'between' => [
|
||||
'numeric' => 'The :attribute must be between :min and :max.',
|
||||
'file' => 'The :attribute must be between :min and :max kilobytes.',
|
||||
'string' => 'The :attribute must be between :min and :max characters.',
|
||||
'array' => 'The :attribute must have between :min and :max items.',
|
||||
],
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'confirmed' => 'The :attribute confirmation does not match.',
|
||||
'date' => 'The :attribute is not a valid date.',
|
||||
'date_equals' => 'The :attribute must be a date equal to :date.',
|
||||
'date_format' => 'The :attribute does not match the format :format.',
|
||||
'different' => 'The :attribute and :other must be different.',
|
||||
'digits' => 'The :attribute must be :digits digits.',
|
||||
'digits_between' => 'The :attribute must be between :min and :max digits.',
|
||||
'dimensions' => 'The :attribute has invalid image dimensions.',
|
||||
'distinct' => 'The :attribute field has a duplicate value.',
|
||||
'email' => 'The :attribute must be a valid email address.',
|
||||
'ends_with' => 'The :attribute must end with one of the following: :values.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'file' => 'The :attribute must be a file.',
|
||||
'filled' => 'The :attribute field must have a value.',
|
||||
'gt' => [
|
||||
'numeric' => 'The :attribute must be greater than :value.',
|
||||
'file' => 'The :attribute must be greater than :value kilobytes.',
|
||||
'string' => 'The :attribute must be greater than :value characters.',
|
||||
'array' => 'The :attribute must have more than :value items.',
|
||||
],
|
||||
'gte' => [
|
||||
'numeric' => 'The :attribute must be greater than or equal :value.',
|
||||
'file' => 'The :attribute must be greater than or equal :value kilobytes.',
|
||||
'string' => 'The :attribute must be greater than or equal :value characters.',
|
||||
'array' => 'The :attribute must have :value items or more.',
|
||||
],
|
||||
'image' => 'The :attribute must be an image.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field does not exist in :other.',
|
||||
'integer' => 'The :attribute must be an integer.',
|
||||
'ip' => 'The :attribute must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute must be a valid IPv4 address.',
|
||||
'ipv6' => 'The :attribute must be a valid IPv6 address.',
|
||||
'json' => 'The :attribute must be a valid JSON string.',
|
||||
'lt' => [
|
||||
'numeric' => 'The :attribute must be less than :value.',
|
||||
'file' => 'The :attribute must be less than :value kilobytes.',
|
||||
'string' => 'The :attribute must be less than :value characters.',
|
||||
'array' => 'The :attribute must have less than :value items.',
|
||||
],
|
||||
'lte' => [
|
||||
'numeric' => 'The :attribute must be less than or equal :value.',
|
||||
'file' => 'The :attribute must be less than or equal :value kilobytes.',
|
||||
'string' => 'The :attribute must be less than or equal :value characters.',
|
||||
'array' => 'The :attribute must not have more than :value items.',
|
||||
],
|
||||
'max' => [
|
||||
'numeric' => 'The :attribute may not be greater than :max.',
|
||||
'file' => 'The :attribute may not be greater than :max kilobytes.',
|
||||
'string' => 'The :attribute may not be greater than :max characters.',
|
||||
'array' => 'The :attribute may not have more than :max items.',
|
||||
],
|
||||
'mimes' => 'The :attribute must be a file of type: :values.',
|
||||
'mimetypes' => 'The :attribute must be a file of type: :values.',
|
||||
'min' => [
|
||||
'numeric' => 'The :attribute must be at least :min.',
|
||||
'file' => 'The :attribute must be at least :min kilobytes.',
|
||||
'string' => 'The :attribute must be at least :min characters.',
|
||||
'array' => 'The :attribute must have at least :min items.',
|
||||
],
|
||||
'not_in' => 'The selected :attribute is invalid.',
|
||||
'not_regex' => 'The :attribute format is invalid.',
|
||||
'numeric' => 'The :attribute must be a number.',
|
||||
'password' => 'The password is incorrect.',
|
||||
'present' => 'The :attribute field must be present.',
|
||||
'regex' => 'The :attribute format is invalid.',
|
||||
'required' => 'The :attribute field is required.',
|
||||
'required_if' => 'The :attribute field is required when :other is :value.',
|
||||
'required_unless' => 'The :attribute field is required unless :other is in :values.',
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_with_all' => 'The :attribute field is required when :values are present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'required_without_all' => 'The :attribute field is required when none of :values are present.',
|
||||
'same' => 'The :attribute and :other must match.',
|
||||
'size' => [
|
||||
'numeric' => 'The :attribute must be :size.',
|
||||
'file' => 'The :attribute must be :size kilobytes.',
|
||||
'string' => 'The :attribute must be :size characters.',
|
||||
'array' => 'The :attribute must contain :size items.',
|
||||
],
|
||||
'starts_with' => 'The :attribute must start with one of the following: :values.',
|
||||
'string' => 'The :attribute must be a string.',
|
||||
'timezone' => 'The :attribute must be a valid zone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
'url' => 'The :attribute format is invalid.',
|
||||
'uuid' => 'The :attribute must be a valid UUID.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap our attribute placeholder
|
||||
| with something more reader friendly such as "E-Mail Address" instead
|
||||
| of "email". This simply helps us make our message more expressive.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
131
resources/lang/fr.json
Executable file
131
resources/lang/fr.json
Executable file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
":user created a bookmark to :document in :breadcrumbs": ":user a créé un signet vers :document dans :breadcrumbs",
|
||||
":user created the folder :breadcrumbs": ":user a créé le dossier :breadcrumbs",
|
||||
":user created the group :group": ":user a créé le groupe :group",
|
||||
":user deleted folder :breadcrumbs": ":user a supprimé le dossier :breadcrumbs",
|
||||
":user follows feed :feed": ":user suit le flux :feed",
|
||||
":user ignores feed :feed": ":user ignore le flux :feed",
|
||||
":user removed the bookmark to :document from :breadcrumbs": ":user a supprimé le signet vers :document dans :breadcrumbs",
|
||||
"A problem occurred, interrupting importation": "Un problème est survenu, importation annulée",
|
||||
"About Cyca": "À propos de Cyca",
|
||||
"Accepted": "Accepté",
|
||||
"Active users": "Utilisateurs actifs",
|
||||
"Add document": "Nouveau document",
|
||||
"Add explicit permissions for": "Ajouter des permissions explicites pour",
|
||||
"Add folder": "Nouveau dossier",
|
||||
"Add highlight": "Ajouter une mise en surbrillance",
|
||||
"Also exists in": "Existe aussi dans",
|
||||
"Apply default permissions": "Permissions par défaut",
|
||||
"Auto-accept users": "Les nouveaux utilisateurs sont automatiquement acceptés",
|
||||
"Awaiting e-mail address confirmation": "En attente de confirmation de votre adresse e-mail",
|
||||
"Back": "Retour",
|
||||
"Cancel": "Annuler",
|
||||
"Color": "Couleur",
|
||||
"Confirm Password": "Confirmation du mot de passe",
|
||||
"Create document": "Créer un document",
|
||||
"Create folder": "Créer un dossier",
|
||||
"Create group": "Créer un groupe",
|
||||
"Create highlight": "Créer une mise en surbrillance",
|
||||
"Created by": "Créé par",
|
||||
"Created by me": "Créé par moi",
|
||||
"Current password": "Mot de passe actuel",
|
||||
"Cyca could not reach this document URL": "Cyca ne peut pas atteindre l'URL de ce document",
|
||||
"Cyca did not check this document yet": "Cyca n'a pas encore vérifié ce document",
|
||||
"Cyca export": "Export Cyca",
|
||||
"Cyca is a Free and Open Source Software released under the :license license in its most recent version.": "Cyca est un Logiciel Libre sous licence :license dans sa version la plus récente.",
|
||||
"Data imported": "Données importées",
|
||||
"Date of bookmark's creation": "Date de création du signet",
|
||||
"Date of bookmark's last access or modification": "Date du dernier accès ou de la dernière modification du signet",
|
||||
"Date of creation": "Date de création",
|
||||
"Date of document's creation": "Date de création du document",
|
||||
"Date of document's last check": "Date de la dernière vérification du document",
|
||||
"Date of item's creation": "Date de création de l'élément",
|
||||
"Date of item's publication": "Date de publication de l'élément",
|
||||
"Date of last access or modification": "Date du dernier accès ou de la dernière modification",
|
||||
"Delete": "Supprimer",
|
||||
"Delete document": "Supprimer un document",
|
||||
"Delete folder": "Supprimer un dossier",
|
||||
"Delete permanently": "Supprimer définitivement",
|
||||
"Description": "Description",
|
||||
"Details": "Détails",
|
||||
"E-Mail Address": "Adresse e-mail",
|
||||
"E-mail address verified on :email_verified_at": "Adresse e-mail vérifiée le :email_verified_at",
|
||||
"Edit": "Modifier",
|
||||
"Error": "Erreur",
|
||||
"Export my data": "Exporter mes données",
|
||||
"Expression": "Expression",
|
||||
"Feeds": "Flux",
|
||||
"File to import": "Fichier à importer",
|
||||
"Follow": "Suivre",
|
||||
"GitHub repository": "Dépôt GitHub",
|
||||
"Group name": "Nom du groupe",
|
||||
"Groups": "Groupes",
|
||||
"HTTP Status Code": "Code de Statut HTTP",
|
||||
"HTTP response": "Réponse HTTP",
|
||||
"Highlights": "Mises en surbrillance",
|
||||
"History": "Historique",
|
||||
"If you like Cyca, maybe you could consider donating": "Si vous aimez Cyca, peut-être pourriez-vous considérer faire un don",
|
||||
"Ignore": "Ignorer",
|
||||
"Import": "Importer",
|
||||
"Import data": "Importer des données",
|
||||
"Import from": "Importer depuis",
|
||||
"Importing, please wait...": "Importation en cours, veuillez patienter...",
|
||||
"Initial URL": "URL initial",
|
||||
"Invite in group": "Inviter dans le groupe",
|
||||
"Invite only": "Seulement sur invitation",
|
||||
"Invited to join": "Invité à rejoindre le groupe",
|
||||
"Language": "Langue",
|
||||
"Leave the group": "Quitter le groupe",
|
||||
"Licenses": "Licences",
|
||||
"Login": "Se connecter",
|
||||
"Logout": "Se déconnecter",
|
||||
"MIME type": "Type MIME",
|
||||
"Mark as read": "Marquer comme lu",
|
||||
"Metadata": "Méta-données",
|
||||
"My account": "Mon compte",
|
||||
"My groups": "Mes groupes",
|
||||
"My own group": "Mon propre groupe",
|
||||
"New password": "Nouveau mot de passe",
|
||||
"New users must be approved": "Les nouveaux utilisateurs doivent être approuvés",
|
||||
"Official website": "Site officiel",
|
||||
"Open": "Ouvrir",
|
||||
"Password": "Mot de passe",
|
||||
"Password lost": "Mot de passe égaré",
|
||||
"Pending users": "Utilisateurs en attente",
|
||||
"Per-user permissions": "Permissions par utilisateur",
|
||||
"Please confirm your password before continuing.": "Veuillez confirmer votre mot de passe avant de continuer.",
|
||||
"Public groups": "Groupes publics",
|
||||
"Published in": "Publié dans",
|
||||
"Real URL": "URL réel",
|
||||
"Register": "Créer un compte",
|
||||
"Remember Me": "Se souvenir de moi",
|
||||
"Remove highlight": "Supprimer une mise en surbrillance",
|
||||
"Reset Password": "Réinitialiser le mot de passe",
|
||||
"Response code": "Code de réponse HTTP",
|
||||
"Response details": "Détails de la réponse",
|
||||
"Restore": "Restaurer",
|
||||
"Root": "Racine",
|
||||
"Save": "Enregistrer",
|
||||
"Search": "Rechercher",
|
||||
"Send Password Reset Link": "Envoyer un e-mail de réinitialisation du mot de passe",
|
||||
"Send email verification link": "Envoyer un e-mail de vérification",
|
||||
"Share": "Partager",
|
||||
"Software used by Cyca can be released under different licenses. Please see below for more informations.": "Les logiciels utilisés par Cyca peuvent être diffusés sous une licence différente. Vous trouverez plus d'informations ci-dessous.",
|
||||
"Thank you for using Cyca !": "Merci d'utiliser Cyca !",
|
||||
"Theme": "Thème",
|
||||
"Unread items": "Éléments non-lus",
|
||||
"Update folder": "Mettre à jour le dossier",
|
||||
"Update group": "Mettre à jour le groupe",
|
||||
"Update password": "Mettre à jour le mot de passe",
|
||||
"User :user has been created": "Utilisateur :user créé",
|
||||
"User Name": "Nom d'utilisateur",
|
||||
"Users without explicit permissions can": "Les utilisateurs sans permissions explicites peuvent",
|
||||
"Via Buy me a coffee": "Par Buy me a coffee",
|
||||
"Via PayPal": "Par PayPal",
|
||||
"Visits": "Visites",
|
||||
"Waiting for joining": "En attente d'acceptation",
|
||||
"We have emailed your password reset link!": "Nous vous avons envoyé un lien de réinitialisation par email!",
|
||||
"You can import the following types of file": "Vous pouvez importer des fichiers de type",
|
||||
"Your profile has been updated": "Votre profil a été mis à jour",
|
||||
"profile-information-updated": "Profil mis à jour"
|
||||
}
|
||||
43
resources/views/account/about.blade.php
Executable file
43
resources/views/account/about.blade.php
Executable file
@@ -0,0 +1,43 @@
|
||||
@extends('layouts.account')
|
||||
|
||||
@section('content')
|
||||
<div class="my-4 h-screen w-1/2 p-6 cyca-prose">
|
||||
<h2>Cyca <small>{{ config('app.version') }}</small></h2>
|
||||
|
||||
<p>{{ __('Created by') }} Richard Dern.</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="https://www.getcyca.com" target="_blank">{{ __("Official website") }}</a></li>
|
||||
<li><a href="https://microblog.getcyca.com/richard" target="_blank">{{ __("Microblog") }}</a></li>
|
||||
<li><a href="https://github.com/RichardDern/Cyca" target="_blank"
|
||||
rel="noopener noreferrer">{{ __("GitHub repository") }}</a></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
{{ __("If you like Cyca, maybe you could consider donating") }}:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><a
|
||||
href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=GFZ3SAY3Y8NAS">{{ __("Via PayPal") }}</a>
|
||||
</li>
|
||||
<li><a href="https://www.buymeacoffee.com/richarddern">{{ __("Via Buy me a coffee") }}</a></li>
|
||||
</ul>
|
||||
|
||||
<p>{{ __("Thank you for using Cyca !") }}</p>
|
||||
|
||||
<h3>{{ __("Licenses") }}</h3>
|
||||
|
||||
<p>{!! __("Cyca is a Free and Open Source Software released under the :license license in its most recent version.",
|
||||
[
|
||||
"license" => '<a href="https://www.gnu.org/licenses/gpl.html" rel="noopener noreferrer" target="_blank">GNU
|
||||
GPL</a>'
|
||||
]) !!}
|
||||
<p>
|
||||
|
||||
<p>{{ __("Software used by Cyca can be released under different licenses. Please see below for more informations.") }}
|
||||
</p>
|
||||
|
||||
<pre>{{ file_get_contents(public_path('/js/app.js.LICENSE.txt')) }}</pre>
|
||||
</div>
|
||||
@endsection
|
||||
9
resources/views/account/groups.blade.php
Executable file
9
resources/views/account/groups.blade.php
Executable file
@@ -0,0 +1,9 @@
|
||||
@extends('layouts.account')
|
||||
|
||||
@push('scripts')
|
||||
<script src="{{ asset('js/groups.js') }}" defer></script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<groups-browser></groups-browser>
|
||||
@endsection
|
||||
9
resources/views/account/highlights.blade.php
Executable file
9
resources/views/account/highlights.blade.php
Executable file
@@ -0,0 +1,9 @@
|
||||
@extends('layouts.account')
|
||||
|
||||
@push('scripts')
|
||||
<script src="{{ asset('js/highlights.js') }}" defer></script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<highlights></highlights>
|
||||
@endsection
|
||||
11
resources/views/account/import.blade.php
Executable file
11
resources/views/account/import.blade.php
Executable file
@@ -0,0 +1,11 @@
|
||||
@extends('layouts.account')
|
||||
|
||||
@push('scripts')
|
||||
<script src="{{ asset('js/import.js') }}" defer></script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="my-auto w-1/4 p-6">
|
||||
<importer v-bind:available-importers='@json(config('importers.adapters'))'></importer>
|
||||
</div>
|
||||
@endsection
|
||||
119
resources/views/account/my_account.blade.php
Executable file
119
resources/views/account/my_account.blade.php
Executable file
@@ -0,0 +1,119 @@
|
||||
@extends('layouts.account')
|
||||
|
||||
@section('content')
|
||||
<div class="my-auto w-1/4 p-6">
|
||||
@if (session('status') === 'profile-information-updated')
|
||||
<div class="alert success">{{ __('Your profile has been updated') }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="/user/profile-information">
|
||||
@method('PUT')
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">
|
||||
{{ __('User Name') }}:
|
||||
</label>
|
||||
|
||||
<input id="name" type="text" name="name" value="{{ old('name', auth()->user()->name) }}" required
|
||||
autocomplete="name" autofocus>
|
||||
</div>
|
||||
|
||||
@error('name')
|
||||
<div class="alert error">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
{{ __('E-Mail Address') }}:
|
||||
</label>
|
||||
|
||||
<input id="email" type="email" name="email" value="{{ old('email', auth()->user()->email) }}" required
|
||||
autocomplete="email">
|
||||
</div>
|
||||
|
||||
@error('email')
|
||||
<div class="alert error">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
@if (!empty(
|
||||
auth()
|
||||
->user()
|
||||
->hasVerifiedEmail()
|
||||
))
|
||||
<p class="text-green-500 italic">{{ __('E-mail address verified on :email_verified_at', [
|
||||
'email_verified_at' => auth()->user()->email_verified_at->isoFormat('LLLL'),
|
||||
]) }}</p>
|
||||
@else
|
||||
<p class="text-orange-500 italic">{{ __('Awaiting e-mail address confirmation') }}</p>
|
||||
@endif
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<label for="lang">
|
||||
{{ __('Language') }}:
|
||||
</label>
|
||||
|
||||
<select name="lang" id="lang">
|
||||
@foreach (config('lang') as $code => $name)
|
||||
<option value="{{ $code }}" {{ auth()->user()->lang === $code ? 'selected' : '' }}>{{ $name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('lang')
|
||||
<div class="alert error">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<label>
|
||||
{{ __('Theme') }}:
|
||||
</label>
|
||||
|
||||
<div class="theme-selector">
|
||||
<label>
|
||||
<input type="radio" name="theme" value="light" {{ auth()->user()->theme === 'light' ? 'checked' : ""
|
||||
}} onchange="localStorage.theme = 'light'; setTheme();" />
|
||||
<div class="ml-1">{{__("Light")}}</div>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="theme" value="dark" {{ auth()->user()->theme === 'dark' ? 'checked' : ""
|
||||
}} onchange="localStorage.theme = 'dark'; setTheme();" />
|
||||
<div class="ml-1">{{__("Dark")}}</div>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="theme" value="auto" {{ !auth()->user()->theme || auth()->user()->theme ===
|
||||
'auto' ? 'checked' : "" }} onchange="localStorage.removeItem('auto'); setTheme();" />
|
||||
<div class="ml-1">{{__("Auto")}}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@error('theme')
|
||||
<div class="alert error">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<div class="form-group last">
|
||||
<button type="submit">→ {{ __('Save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (!auth()
|
||||
->user()
|
||||
->hasVerifiedEmail())
|
||||
<a href="#" class="button info p-2 mt-12"
|
||||
onclick="event.preventDefault(); document.getElementById('resend-verification-form').submit();">{{ __('Send email verification link') }}</a>
|
||||
|
||||
<form id="resend-verification-form" method="POST" action="{{ route('verification.send') }}" class="hidden">
|
||||
@csrf
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
57
resources/views/account/password.blade.php
Executable file
57
resources/views/account/password.blade.php
Executable file
@@ -0,0 +1,57 @@
|
||||
@extends('layouts.account')
|
||||
|
||||
@section('content')
|
||||
<div class="my-auto w-1/4 p-6">
|
||||
@if (session('status') === 'password-updated')
|
||||
<div class="alert success">{{ __('Your profile has been updated') }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" class="w-full flex flex-col" action="/user/password">
|
||||
@method('PUT')
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label for="current-password">
|
||||
{{ __('Current password') }}:
|
||||
</label>
|
||||
|
||||
<input id="current-password" type="password" name="current_password" required />
|
||||
</div>
|
||||
|
||||
@error('current_password')
|
||||
<div class="alert error">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-password">
|
||||
{{ __('New password') }}:
|
||||
</label>
|
||||
|
||||
<input id="new-password" type="password" required autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password-confirm">
|
||||
{{ __('Confirm Password') }}:
|
||||
</label>
|
||||
|
||||
<input id="password-confirm" type="password" name="password_confirmation" required
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
@error('password_confirmation')
|
||||
<div class="alert error">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<div class="form-group last">
|
||||
<button type="submit">
|
||||
→ {{ __('Update password') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
36
resources/views/auth/forgot-password.blade.php
Executable file
36
resources/views/auth/forgot-password.blade.php
Executable file
@@ -0,0 +1,36 @@
|
||||
@extends('layouts.auth')
|
||||
|
||||
@section('content')
|
||||
<div class="my-auto w-1/4 p-6">
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
{{ __('E-Mail Address') }}:
|
||||
</label>
|
||||
|
||||
<input id="email" type="email" class="@error('email') border-red-500 @enderror" name="email"
|
||||
value="{{ old('email') }}" required autocomplete="email" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group last">
|
||||
<button type="submit">
|
||||
→ {{ __('Send Password Reset Link') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@error('email')
|
||||
<p class="text-red-500 text-xs italic mt-4">
|
||||
{{ $message }}
|
||||
</p>
|
||||
@enderror
|
||||
|
||||
@if (session('status'))
|
||||
<p class="text-sm text-green-500 mt-4" role="alert">
|
||||
{{ session('status') }}
|
||||
</p>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
52
resources/views/auth/login.blade.php
Executable file
52
resources/views/auth/login.blade.php
Executable file
@@ -0,0 +1,52 @@
|
||||
@extends('layouts.auth')
|
||||
|
||||
@section('content')
|
||||
<div class="my-auto w-1/4 p-6">
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
{{ __('E-Mail Address') }}:
|
||||
</label>
|
||||
|
||||
<input id="email" type="email" class="@error('email') border-red-500 @enderror" name="email"
|
||||
value="{{ old('email') }}" required autocomplete="email" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">
|
||||
{{ __('Password') }}:
|
||||
</label>
|
||||
|
||||
<input id="password" type="password" class="@error('password') border-red-500 @enderror" name="password"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="remember">
|
||||
<input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
|
||||
<span class="ml-2">{{ __('Remember Me') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group last">
|
||||
<button type="submit">
|
||||
→ {{ __('Login') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@error('email')
|
||||
<p class="text-red-500 text-xs italic mt-4">
|
||||
{{ $message }}
|
||||
</p>
|
||||
@enderror
|
||||
|
||||
@error('password')
|
||||
<p class="text-red-500 text-xs italic mt-4">
|
||||
{{ $message }}
|
||||
</p>
|
||||
@enderror
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
69
resources/views/auth/register.blade.php
Executable file
69
resources/views/auth/register.blade.php
Executable file
@@ -0,0 +1,69 @@
|
||||
@extends('layouts.auth')
|
||||
|
||||
@section('content')
|
||||
<div class="my-auto w-1/4 p-6">
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">
|
||||
{{ __('User Name') }}:
|
||||
</label>
|
||||
|
||||
<input id="name" type="text" class="@error('name') border-red-500 @enderror" name="name"
|
||||
value="{{ old('name') }}" required autocomplete="name" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
{{ __('E-Mail Address') }}:
|
||||
</label>
|
||||
|
||||
<input id="email" type="email" class="@error('email') border-red-500 @enderror" name="email"
|
||||
value="{{ old('email') }}" required autocomplete="email">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">
|
||||
{{ __('Password') }}:
|
||||
</label>
|
||||
|
||||
<input id="password" type="password" class="@error('password') border-red-500 @enderror" name="password"
|
||||
required autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password-confirm">
|
||||
{{ __('Confirm Password') }}:
|
||||
</label>
|
||||
|
||||
<input id="password-confirm" type="password" name="password_confirmation" required
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group last">
|
||||
<button type="submit">
|
||||
→ {{ __('Register') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@error('name')
|
||||
<p class="text-red-500 text-xs italic mt-4">
|
||||
{{ $message }}
|
||||
</p>
|
||||
@enderror
|
||||
|
||||
@error('email')
|
||||
<p class="text-red-500 text-xs italic mt-4">
|
||||
{{ $message }}
|
||||
</p>
|
||||
@enderror
|
||||
|
||||
@error('password')
|
||||
<p class="text-red-500 text-xs italic mt-4">
|
||||
{{ $message }}
|
||||
</p>
|
||||
@enderror
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
54
resources/views/auth/reset-password.blade.php
Executable file
54
resources/views/auth/reset-password.blade.php
Executable file
@@ -0,0 +1,54 @@
|
||||
@extends('layouts.auth')
|
||||
|
||||
@section('content')
|
||||
<div class="my-auto w-1/4 p-6">
|
||||
<form method="POST" action="{{ route('password.update') }}">
|
||||
@csrf
|
||||
|
||||
<input id="email" type="hidden" name="email" value="{{ request()->input('email') }}" required>
|
||||
<input id="token" type="hidden" name="token" value="{{ request()->route('token') }}" required>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">
|
||||
{{ __('New password') }}:
|
||||
</label>
|
||||
|
||||
<input id="password" type="password" class="@error('password') border-red-500 @enderror" name="password"
|
||||
autofocus required autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password-confirm">
|
||||
{{ __('Confirm Password') }}:
|
||||
</label>
|
||||
|
||||
<input id="password-confirm" type="password" name="password_confirmation" required
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group last">
|
||||
<button type="submit">
|
||||
{{ __('Reset Password') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@error('email')
|
||||
<p class="text-red-500 text-xs italic mt-4">
|
||||
{{ $message }}
|
||||
</p>
|
||||
@enderror
|
||||
|
||||
@error('password')
|
||||
<p class="text-red-500 text-xs italic mt-4">
|
||||
{{ $message }}
|
||||
</p>
|
||||
@enderror
|
||||
|
||||
@if (session('status'))
|
||||
<p class="text-sm text-green-500 mt-4" role="alert">
|
||||
{{ session('status') }}
|
||||
</p>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user