Refresh
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user