mirror of
https://github.com/directus/directus.git
synced 2026-01-28 06:08:01 -05:00
Various style tweaks (#555)
* icon width * updated pagination style * file preview zoom WIP — shouldn’t show up on MODAL preview * overlay/modal close button styling * duplicate key * bookmark styling * card fade also adds an rgb value for the page background variable * style per page dropdown * cards per page dropdown color * inset non-dense notifications within sidebar * reduce border radius for xs avatars * hide non-expanded prepend/append * reduce sidebar padding this gives content a bit more room * WIP: split and update comments and revisions work in progress * fix collections module name * fix file library title * consistent border on disabled * fix title/breadcrumb positioning * breadcrumb fixes * add “open” button to image interface WIP — this needs the actual logic, and we might want to remove a button * hide presets delete until selection * image shadow and subtext color * Remove breadcrumb calculation * increase contrast for image subtitle * fix textarea hover style * Update src/modules/collections/index.ts * Fix typing of translateresult to format * Add undefined check to collection name * Put v-if on dialog instead of button * Remove breadcrumb logic * Remove breadcrumb calculation * Rename shadow to collapsed in header bar * fix rating star display going over table header * show collection breadcrumb for bookmarks WIP — needs the formatted collection title * shorter error to avoid wrapping * remove periods * Fix standard font-size of divider label, use large in interface * Add extra date format strings * Structure comments, support date headers * Add ability to delete comments * Add edited on status badge Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
</div>
|
||||
</transition>
|
||||
<router-view v-if="!hydrating" />
|
||||
<portal-target name="dialog-outlet" multiple />
|
||||
<portal-target name="popper-outlet" multiple />
|
||||
|
||||
<portal-target name="outlet" multiple />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ body {
|
||||
|
||||
&.x-small {
|
||||
--v-avatar-size: 24px;
|
||||
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.small {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const basic = () =>
|
||||
<v-button @click="active = false">Yes</v-button>
|
||||
</v-sheet>
|
||||
</v-dialog>
|
||||
<portal-target name="dialog-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
@@ -66,7 +66,7 @@ export const activatorSlot = () =>
|
||||
<v-button @click="active = false">Yes</v-button>
|
||||
</v-sheet>
|
||||
</v-dialog>
|
||||
<portal-target name="dialog-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="v-dialog">
|
||||
<slot name="activator" v-bind="{ on: () => $emit('toggle', true) }" />
|
||||
|
||||
<portal to="dialog-outlet">
|
||||
<portal to="outlet">
|
||||
<transition name="dialog">
|
||||
<div v-if="active" class="container" :class="[className]">
|
||||
<v-overlay active absolute @click="emitToggle" />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="v-divider" :class="{ vertical, inlineTitle }">
|
||||
<div class="v-divider" :class="{ vertical, inlineTitle, large }">
|
||||
<hr role="separator" :aria-orientation="vertical ? 'vertical' : 'horizontal'" />
|
||||
<span v-if="$slots.icon || $slots.default" class="wrapper">
|
||||
<slot name="icon" class="icon" />
|
||||
<span v-if="!vertical && $slots.default" class="title"><slot /></span>
|
||||
<span v-if="!vertical && $slots.default" class="type-text"><slot /></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,6 +21,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -53,8 +57,14 @@ body {
|
||||
|
||||
span.wrapper {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
color: var(--v-divider-label-color);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&.large .type-text {
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ export const withMenu = () =>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<portal-target name="popper-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ export const basic = () =>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<portal-target name="popper-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
@@ -72,7 +72,7 @@ export const withVModel = () =>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<portal-target name="popper-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
@@ -124,7 +124,7 @@ export const positioning = () =>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<portal-target name="popper-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
@@ -186,7 +186,7 @@ export const withEdgeOffset = () =>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
<portal-target name="popper-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
@@ -225,7 +225,7 @@ export const attached = () =>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<portal-target name="popper-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<portal to="popper-outlet">
|
||||
<portal to="outlet">
|
||||
<transition name="bounce">
|
||||
<div
|
||||
class="v-menu-popper"
|
||||
|
||||
@@ -33,7 +33,7 @@ export const basic = () =>
|
||||
<v-button @click="close">Close modal</v-button>
|
||||
</template>
|
||||
</v-modal>
|
||||
<portal-target name="dialog-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
@@ -78,7 +78,7 @@ export const withNav = () =>
|
||||
<v-button @click="close">Close modal</v-button>
|
||||
</template>
|
||||
</v-modal>
|
||||
<portal-target name="dialog-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -137,9 +137,9 @@ body {
|
||||
}
|
||||
|
||||
.v-button {
|
||||
--v-button-background-color-hover: var(--background-subdued);
|
||||
--v-button-background-color-hover: var(--background-normal);
|
||||
--v-button-background-color: var(--background-subdued);
|
||||
--v-button-color: var(--foreground-subdued);
|
||||
--v-button-color: var(--foreground-normal);
|
||||
|
||||
margin: 0 2px;
|
||||
vertical-align: middle;
|
||||
@@ -159,10 +159,10 @@ body {
|
||||
}
|
||||
|
||||
&.active {
|
||||
--v-button-background-color-hover: var(--primary-25);
|
||||
--v-button-color-hover: var(--primary);
|
||||
--v-button-background-color: var(--primary-25);
|
||||
--v-button-color: var(--primary);
|
||||
--v-button-background-color-hover: var(--foreground-normal);
|
||||
--v-button-color-hover: var(--foreground-inverted);
|
||||
--v-button-background-color: var(--foreground-normal);
|
||||
--v-button-color: var(--foreground-inverted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export const basic = () =>
|
||||
:inline="inline"
|
||||
/>
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
<portal-target name="popper-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
@@ -89,7 +89,7 @@ export const multiple = () =>
|
||||
multiple
|
||||
/>
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
<portal-target name="popper-outlet" />
|
||||
<portal-target name="outlet" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -302,7 +302,7 @@ export default defineComponent({
|
||||
.fixed {
|
||||
position: sticky;
|
||||
top: var(--v-table-sticky-offset-top);
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.manual {
|
||||
|
||||
@@ -112,6 +112,7 @@ body {
|
||||
.prepend {
|
||||
opacity: 0;
|
||||
transition: opacity var(--medium) var(--transition);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
@@ -122,12 +123,13 @@ body {
|
||||
.append,
|
||||
.prepend {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-normal);
|
||||
border-color: var(--border-normal-alt);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
@@ -142,7 +144,6 @@ body {
|
||||
&.disabled {
|
||||
color: var(--foreground-subdued);
|
||||
background-color: var(--background-subdued);
|
||||
border-color: var(--border-subdued);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
'--v-divider-color': color,
|
||||
'--v-divider-label-color': color,
|
||||
}"
|
||||
large
|
||||
>
|
||||
<template v-if="icon" #icon><v-icon :name="icon" /></template>
|
||||
<template v-if="title" #default>{{ title }}</template>
|
||||
|
||||
@@ -4,16 +4,25 @@
|
||||
<img :src="src" alt="" role="presentation" />
|
||||
<div class="shadow" />
|
||||
<div class="actions">
|
||||
<v-button icon rounded @click="editorActive = true">
|
||||
<v-icon name="crop_rotate" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="lightboxActive = true">
|
||||
<v-button icon rounded @click="lightboxActive = true" v-tooltip="$t('zoom')">
|
||||
<v-icon name="zoom_in" />
|
||||
</v-button>
|
||||
<v-button icon rounded :href="image.data.full_url" :download="image.filename_download">
|
||||
<v-button
|
||||
icon
|
||||
rounded
|
||||
:href="image.data.full_url"
|
||||
:download="image.filename_download"
|
||||
v-tooltip="$t('download')"
|
||||
>
|
||||
<v-icon name="file_download" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="deselect">
|
||||
<v-button icon rounded @click="lightboxActive = true" v-tooltip="$t('open')">
|
||||
<v-icon name="launch" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="editorActive = true" v-tooltip="$t('edit')">
|
||||
<v-icon name="crop_rotate" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="deselect" v-tooltip="$t('deselect')">
|
||||
<v-icon name="close" />
|
||||
</v-button>
|
||||
</div>
|
||||
@@ -216,7 +225,7 @@ img {
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
background: linear-gradient(180deg, rgba(38, 50, 56, 0) 0%, rgba(38, 50, 56, 0.75) 100%);
|
||||
background: linear-gradient(180deg, rgba(38, 50, 56, 0) 0%, rgba(38, 50, 56, 0.25) 100%);
|
||||
transition: height var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
@@ -271,13 +280,14 @@ img {
|
||||
height: 17px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
color: var(--foreground-subdued);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
transition: max-height var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
.image-preview:hover {
|
||||
.shadow {
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, rgba(38, 50, 56, 0) 0%, rgba(38, 50, 56, 0.5) 100%);
|
||||
}
|
||||
|
||||
.actions .v-button {
|
||||
|
||||
@@ -72,11 +72,13 @@
|
||||
"settings_update_success": "Settings updated",
|
||||
"settings_update_failed": "Updating settings failed",
|
||||
|
||||
"activity_delta_created": "Created this item",
|
||||
"activity_delta_created_externally": "Created externally",
|
||||
"activity_delta_updated": "Updated this item",
|
||||
"activity_delta_deleted": "Deleted this item",
|
||||
"revision_delta_created": "Created",
|
||||
"revision_delta_created_externally": "Created Externally",
|
||||
"revision_delta_updated": "Updated {count} Fields",
|
||||
"revision_delta_soft_deleted": "Soft Deleted",
|
||||
"revision_delta_deleted": "Deleted",
|
||||
"private_user": "Private User",
|
||||
"revision_unknown": "Created Outside System",
|
||||
|
||||
"leave_comment": "Leave a comment...",
|
||||
"post_comment_success": "Comment posted",
|
||||
@@ -99,8 +101,17 @@
|
||||
"corresponding_field_name": "Corresponding Field Name",
|
||||
|
||||
"datetime": "DateTime",
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
|
||||
"delete_comment": "Delete Comment",
|
||||
|
||||
"date-fns_datetime": "PPP h:mma",
|
||||
"date-fns_date": "PPP",
|
||||
"date-fns_time": "K:mm a",
|
||||
"date-fns_date_short": "MMM d, u",
|
||||
"date-fns_date_short_no_year": "MMM d",
|
||||
"date-fns_time": "h:mma",
|
||||
|
||||
"month": "Month",
|
||||
"date": "Date",
|
||||
"year": "Year",
|
||||
@@ -126,6 +137,10 @@
|
||||
|
||||
"set_to_now": "Set to Now",
|
||||
|
||||
"zoom": "Zoom",
|
||||
"download": "Download",
|
||||
"open": "Open",
|
||||
|
||||
"name": "Name",
|
||||
"primary_key_field": "Primary Key Field",
|
||||
"type": "Type",
|
||||
@@ -151,6 +166,8 @@
|
||||
"add_existing": "Add Existing",
|
||||
|
||||
"comments": "Comments",
|
||||
"no_comments": "No Comments Yet",
|
||||
"click_to_expand": "Click to Expand",
|
||||
|
||||
"select_item": "Select Item",
|
||||
|
||||
@@ -166,8 +183,8 @@
|
||||
"radio_buttons": "Radio Buttons",
|
||||
"checkboxes": "Checkboxes",
|
||||
|
||||
"relationship_not_setup": "The relationship hasn't been configured correctly.",
|
||||
"display_template_not_setup": "The display template hasn't been configured correctly.",
|
||||
"relationship_not_setup": "The relationship hasn't been configured correctly",
|
||||
"display_template_not_setup": "The display template is misconfigured",
|
||||
|
||||
"choose_status": "Choose Status...",
|
||||
|
||||
@@ -835,7 +852,7 @@
|
||||
"reset": "Reset",
|
||||
"reset_password": "Reset Password",
|
||||
"reset_preferences": "Reset all listing preferences",
|
||||
|
||||
"revisions": "Revisions",
|
||||
"revert": "Revert",
|
||||
"revert_copy": "Do you want to revert this item to how it was on {date}?",
|
||||
"role_count": "No Roles | One Role | {count} Roles",
|
||||
|
||||
@@ -467,6 +467,10 @@ export default defineComponent({
|
||||
width: auto;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.v-select {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,12 +274,26 @@ export default defineComponent({
|
||||
|
||||
.title,
|
||||
.subtitle {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
line-height: 1.3em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(var(--background-page-rgb), 0) 0%,
|
||||
rgba(var(--background-page-rgb), 1) 100%
|
||||
);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
@@ -565,6 +565,10 @@ export default defineComponent({
|
||||
width: auto;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.v-select {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<private-view :title="$t('activity')">
|
||||
<private-view :title="$t('activity_log')">
|
||||
<template #title-outer:prepend>
|
||||
<v-button rounded disabled icon secondary>
|
||||
<v-button class="header-icon" rounded icon secondary disabled>
|
||||
<v-icon name="notifications" />
|
||||
</v-button>
|
||||
</template>
|
||||
@@ -82,6 +82,12 @@ export default defineComponent({
|
||||
--v-button-background-color-hover: var(--warning-150);
|
||||
}
|
||||
|
||||
.header-icon.secondary {
|
||||
--v-button-background-color: var(--background-normal);
|
||||
--v-button-color-disabled: var(--foreground-normal);
|
||||
--v-button-color-activated: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.layout {
|
||||
--layout-offset-top: 64px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<private-view :title="$t('editing', { collection: $t('activity') })">
|
||||
<private-view title="Updated: Item Display Template">
|
||||
<template #title-outer:prepend>
|
||||
<v-button rounded icon secondary exact :to="breadcrumb[0].to">
|
||||
<v-button class="header-icon" rounded icon secondary exact :to="breadcrumb[0].to">
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
</template>
|
||||
@@ -62,7 +62,7 @@ export default defineComponent({
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
name: i18n.t('activity'),
|
||||
name: i18n.t('activity_log'),
|
||||
to: `/${currentProjectKey.value}/activity/`,
|
||||
},
|
||||
]);
|
||||
@@ -79,6 +79,12 @@ export default defineComponent({
|
||||
--v-button-background-color-hover: var(--danger-dark);
|
||||
}
|
||||
|
||||
.header-icon.secondary {
|
||||
--v-button-background-color: var(--background-normal);
|
||||
--v-button-color-disabled: var(--foreground-normal);
|
||||
--v-button-color-activated: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.v-form {
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import CollectionsItemNotFound from './routes/not-found';
|
||||
|
||||
export default defineModule(({ i18n }) => ({
|
||||
id: 'collections',
|
||||
name: i18n.tc('collection', 2),
|
||||
name: i18n.t('collections'),
|
||||
icon: 'box',
|
||||
routes: [
|
||||
{
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template v-if="bookmark" #headline>
|
||||
<v-breadcrumb :items="breadcrumb" />
|
||||
</template>
|
||||
|
||||
<template #title-outer:append>
|
||||
<bookmark-add
|
||||
v-if="!bookmark"
|
||||
@@ -192,6 +196,7 @@ export default defineComponent({
|
||||
const { selection } = useSelection();
|
||||
const { info: currentCollection, primaryKeyField } = useCollection(collection);
|
||||
const { addNewLink, batchLink, collectionsLink, currentCollectionLink } = useLinks();
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
const {
|
||||
viewType,
|
||||
viewOptions,
|
||||
@@ -243,12 +248,24 @@ export default defineComponent({
|
||||
bookmarkName,
|
||||
editingBookmark,
|
||||
editBookmark,
|
||||
breadcrumb,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
name: props.collection,
|
||||
to: `/${projectsStore.state.currentProjectKey}/collections/${props.collection}`,
|
||||
},
|
||||
]);
|
||||
|
||||
return { breadcrumb };
|
||||
}
|
||||
|
||||
function useSelection() {
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
// Whenever the collection changes we're working on, we have to clear the selection
|
||||
// Whenever the collection we're working on changes, we have to clear the selection
|
||||
watch(
|
||||
() => props.collection,
|
||||
() => (selection.value = [])
|
||||
@@ -387,5 +404,18 @@ export default defineComponent({
|
||||
.bookmark-add .toggle,
|
||||
.bookmark-edit .toggle {
|
||||
margin-left: 8px;
|
||||
transition: color var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
.bookmark-add {
|
||||
color: var(--foreground-subdued);
|
||||
|
||||
&:hover {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.bookmark-edit {
|
||||
color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #headline>
|
||||
<v-breadcrumb :items="breadcrumb" />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-dialog v-if="!isNew" v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
@@ -143,7 +147,12 @@
|
||||
/>
|
||||
|
||||
<template #drawer>
|
||||
<activity-drawer-detail
|
||||
<revisions-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
:collection="collection"
|
||||
:primary-key="primaryKey"
|
||||
/>
|
||||
<comments-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
:collection="collection"
|
||||
:primary-key="primaryKey"
|
||||
@@ -159,7 +168,8 @@ import CollectionsNavigation from '../../components/navigation/';
|
||||
import router from '@/router';
|
||||
import CollectionsNotFound from '../not-found/';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import ActivityDrawerDetail from '@/views/private/components/activity-drawer-detail';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import CommentsDrawerDetail from '@/views/private/components/comments-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import SaveOptions from '@/views/private/components/save-options';
|
||||
|
||||
@@ -169,7 +179,13 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'collections-detail',
|
||||
components: { CollectionsNavigation, CollectionsNotFound, ActivityDrawerDetail, SaveOptions },
|
||||
components: {
|
||||
CollectionsNavigation,
|
||||
CollectionsNotFound,
|
||||
RevisionsDrawerDetail,
|
||||
CommentsDrawerDetail,
|
||||
SaveOptions,
|
||||
},
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
@@ -184,6 +200,7 @@ export default defineComponent({
|
||||
const projectsStore = useProjectsStore();
|
||||
const { currentProjectKey } = toRefs(projectsStore.state);
|
||||
const { collection, primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
const { info: collectionInfo, softDeleteStatus, primaryKeyField } = useCollection(
|
||||
collection
|
||||
@@ -242,8 +259,20 @@ export default defineComponent({
|
||||
isBatch,
|
||||
softDeleteStatus,
|
||||
templateValues,
|
||||
breadcrumb,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
name: collectionInfo.value?.name,
|
||||
to: `/${currentProjectKey.value}/collections/${props.collection}`,
|
||||
},
|
||||
]);
|
||||
|
||||
return { breadcrumb };
|
||||
}
|
||||
|
||||
async function saveAndQuit() {
|
||||
await save();
|
||||
router.push(`/${currentProjectKey.value}/collections/${props.collection}`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="$t('files')">
|
||||
<private-view :title="$t('file_library')">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded disabled icon secondary>
|
||||
<v-icon name="folder" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="loading ? $t('loading') : $t('editing_file', { title: item.title })">
|
||||
<private-view :title="loading ? $t('loading') : item.title">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon secondary exact :to="breadcrumb[0].to">
|
||||
<v-icon name="arrow_back" />
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
|
||||
<template #drawer>
|
||||
<activity-drawer-detail
|
||||
<revisions-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
collection="directus_files"
|
||||
:primary-key="primaryKey"
|
||||
@@ -118,7 +118,7 @@ import useProjectsStore from '@/stores/projects';
|
||||
import FilesNavigation from '../../components/navigation/';
|
||||
import { i18n } from '@/lang';
|
||||
import router from '@/router';
|
||||
import ActivityDrawerDetail from '@/views/private/components/activity-drawer-detail';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import SaveOptions from '@/views/private/components/save-options';
|
||||
import FilePreview from '@/views/private/components/file-preview';
|
||||
@@ -134,7 +134,7 @@ export default defineComponent({
|
||||
name: 'files-detail',
|
||||
components: {
|
||||
FilesNavigation,
|
||||
ActivityDrawerDetail,
|
||||
RevisionsDrawerDetail,
|
||||
SaveOptions,
|
||||
FilePreview,
|
||||
ImageEditor,
|
||||
@@ -206,7 +206,7 @@ export default defineComponent({
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
name: i18n.t('files'),
|
||||
name: i18n.t('file_library'),
|
||||
to: `/${currentProjectKey.value}/files/`,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #headline>
|
||||
<v-breadcrumb :items="breadcrumb" />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
@@ -80,7 +76,6 @@ import SettingsNavigation from '../../../components/navigation/';
|
||||
import useCollection from '@/composables/use-collection/';
|
||||
import FieldsManagement from './components/fields-management';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import { i18n } from '@/lang';
|
||||
import useItem from '@/composables/use-item';
|
||||
import router from '@/router';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
@@ -100,15 +95,6 @@ export default defineComponent({
|
||||
const { currentProjectKey } = toRefs(projectsStore.state);
|
||||
const collectionsStore = useCollectionsStore();
|
||||
|
||||
const breadcrumb = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: i18n.t('settings_data_model'),
|
||||
to: `/${projectsStore.state.currentProjectKey}/settings/data-model`,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const {
|
||||
isNew,
|
||||
edits,
|
||||
@@ -130,7 +116,6 @@ export default defineComponent({
|
||||
return {
|
||||
collectionInfo,
|
||||
fields,
|
||||
breadcrumb,
|
||||
confirmDelete,
|
||||
isNew,
|
||||
edits,
|
||||
|
||||
@@ -7,15 +7,9 @@
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<v-dialog v-model="confirmDelete" v-if="selection.length > 0">
|
||||
<template #activator="{ on }">
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
class="action-delete"
|
||||
@click="on"
|
||||
:disabled="selection.length === 0"
|
||||
>
|
||||
<v-button rounded icon class="action-delete" @click="on">
|
||||
<v-icon name="delete" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<template>
|
||||
<private-view :title="$t('editing', { collection: $t('roles') })">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon exact :to="breadcrumb[0].to">
|
||||
<v-button
|
||||
class="header-icon"
|
||||
rounded
|
||||
icon
|
||||
exact
|
||||
:to="`/${currentProjectKey}/settings/roles/`"
|
||||
>
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #headline>
|
||||
<v-breadcrumb :items="breadcrumb" />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
@@ -80,7 +82,7 @@
|
||||
</div>
|
||||
|
||||
<template #drawer>
|
||||
<activity-drawer-detail
|
||||
<revisions-drawer-detail
|
||||
v-if="isNew === false"
|
||||
collection="directus_roles"
|
||||
:primary-key="primaryKey"
|
||||
@@ -93,9 +95,8 @@
|
||||
import { defineComponent, computed, toRefs, ref } from '@vue/composition-api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import SettingsNavigation from '../../../components/navigation/';
|
||||
import { i18n } from '@/lang';
|
||||
import router from '@/router';
|
||||
import ActivityDrawerDetail from '@/views/private/components/activity-drawer-detail';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import SaveOptions from '@/views/private/components/save-options';
|
||||
import PermissionsManagement from './components/permissions-management';
|
||||
@@ -106,7 +107,7 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'roles-detail',
|
||||
components: { SettingsNavigation, ActivityDrawerDetail, SaveOptions, PermissionsManagement },
|
||||
components: { SettingsNavigation, RevisionsDrawerDetail, SaveOptions, PermissionsManagement },
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
@@ -117,7 +118,6 @@ export default defineComponent({
|
||||
const projectsStore = useProjectsStore();
|
||||
const { currentProjectKey } = toRefs(projectsStore.state);
|
||||
const { primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
const {
|
||||
isNew,
|
||||
@@ -142,7 +142,6 @@ export default defineComponent({
|
||||
loading,
|
||||
error,
|
||||
isNew,
|
||||
breadcrumb,
|
||||
edits,
|
||||
hasEdits,
|
||||
saving,
|
||||
@@ -154,19 +153,9 @@ export default defineComponent({
|
||||
saveAndAddNew,
|
||||
saveAsCopyAndNavigate,
|
||||
isBatch,
|
||||
currentProjectKey,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
name: i18n.t('roles'),
|
||||
to: `/${currentProjectKey.value}/settings/roles/`,
|
||||
},
|
||||
]);
|
||||
|
||||
return { breadcrumb };
|
||||
}
|
||||
|
||||
async function saveAndQuit() {
|
||||
await save();
|
||||
router.push(`/${currentProjectKey.value}/settings/roles`);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<template>
|
||||
<private-view :title="$t('editing', { collection: $t('webhooks') })">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon exact :to="breadcrumb[0].to">
|
||||
<v-button
|
||||
class="header-icon"
|
||||
rounded
|
||||
icon
|
||||
exact
|
||||
:to="`/${currentProjectKey}/settings/webhooks/`"
|
||||
>
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #headline>
|
||||
<v-breadcrumb :items="breadcrumb" />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
@@ -72,7 +74,7 @@
|
||||
/>
|
||||
|
||||
<template #drawer>
|
||||
<activity-drawer-detail
|
||||
<revisions-drawer-detail
|
||||
v-if="isNew === false"
|
||||
collection="directus_webhooks"
|
||||
:primary-key="primaryKey"
|
||||
@@ -85,9 +87,8 @@
|
||||
import { defineComponent, computed, toRefs, ref } from '@vue/composition-api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import SettingsNavigation from '../../../components/navigation/';
|
||||
import { i18n } from '@/lang';
|
||||
import router from '@/router';
|
||||
import ActivityDrawerDetail from '@/views/private/components/activity-drawer-detail';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import SaveOptions from '@/views/private/components/save-options';
|
||||
|
||||
@@ -97,7 +98,7 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'webhooks-detail',
|
||||
components: { SettingsNavigation, ActivityDrawerDetail, SaveOptions },
|
||||
components: { SettingsNavigation, RevisionsDrawerDetail, SaveOptions },
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
@@ -108,7 +109,6 @@ export default defineComponent({
|
||||
const projectsStore = useProjectsStore();
|
||||
const { currentProjectKey } = toRefs(projectsStore.state);
|
||||
const { primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
const {
|
||||
isNew,
|
||||
@@ -133,7 +133,6 @@ export default defineComponent({
|
||||
loading,
|
||||
error,
|
||||
isNew,
|
||||
breadcrumb,
|
||||
edits,
|
||||
hasEdits,
|
||||
saving,
|
||||
@@ -145,19 +144,9 @@ export default defineComponent({
|
||||
saveAndAddNew,
|
||||
saveAsCopyAndNavigate,
|
||||
isBatch,
|
||||
currentProjectKey,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
name: i18n.t('webhooks'),
|
||||
to: `/${currentProjectKey.value}/settings/webhooks/`,
|
||||
},
|
||||
]);
|
||||
|
||||
return { breadcrumb };
|
||||
}
|
||||
|
||||
async function saveAndQuit() {
|
||||
await save();
|
||||
router.push(`/${currentProjectKey.value}/settings/webhooks`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="$t('users')">
|
||||
<private-view :title="$t('user_directory')">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded disabled icon secondary>
|
||||
<v-icon name="people" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="$t('editing', { collection: $t('users') })">
|
||||
<private-view :title="loading ? $t('loading') : item.first_name + ' ' + item.last_name">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon secondary exact :to="breadcrumb[0].to">
|
||||
<v-icon name="arrow_back" />
|
||||
@@ -72,7 +72,7 @@
|
||||
/>
|
||||
|
||||
<template #drawer>
|
||||
<activity-drawer-detail
|
||||
<revisions-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
collection="directus_users"
|
||||
:primary-key="primaryKey"
|
||||
@@ -87,7 +87,7 @@ import useProjectsStore from '@/stores/projects';
|
||||
import UsersNavigation from '../../components/navigation/';
|
||||
import { i18n } from '@/lang';
|
||||
import router from '@/router';
|
||||
import ActivityDrawerDetail from '@/views/private/components/activity-drawer-detail';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import SaveOptions from '@/views/private/components/save-options';
|
||||
|
||||
@@ -97,7 +97,7 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'users-detail',
|
||||
components: { UsersNavigation, ActivityDrawerDetail, SaveOptions },
|
||||
components: { UsersNavigation, RevisionsDrawerDetail, SaveOptions },
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
@@ -150,7 +150,7 @@ export default defineComponent({
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
name: i18n.t('users'),
|
||||
name: i18n.t('user_directory'),
|
||||
to: `/${currentProjectKey.value}/users/`,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
--background-subdued: #F5F7F8;
|
||||
--background-highlight: #F9FAFB;
|
||||
--background-page: #FFF;
|
||||
--background-page-rgb: 255, 255, 255;
|
||||
--background-inverted: #263238;
|
||||
|
||||
--primary-10: #EAF2FD;
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
<template>
|
||||
<drawer-detail :title="$t('comments')" icon="mode_comment">
|
||||
<form @submit.prevent="postComment">
|
||||
<v-textarea
|
||||
:placeholder="$t('leave_comment')"
|
||||
v-model="newCommentContent"
|
||||
expand-on-focus
|
||||
>
|
||||
<template #append>
|
||||
<v-button
|
||||
:disabled="!newCommentContent || newCommentContent.length === 0"
|
||||
:loading="saving"
|
||||
class="post-comment"
|
||||
@click="postComment"
|
||||
x-small
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</v-button>
|
||||
</template>
|
||||
</v-textarea>
|
||||
</form>
|
||||
<transition-group name="slide" tag="div">
|
||||
<div class="activity-record" v-for="act in activity" :key="act.id">
|
||||
<div class="activity-header">
|
||||
<v-avatar small>
|
||||
<v-icon name="person_outline" />
|
||||
</v-avatar>
|
||||
<div class="header-content">
|
||||
<div class="name">
|
||||
<template v-if="act.action_by && act.action_by">
|
||||
{{ act.action_by.first_name }} {{ act.action_by.last_name }}
|
||||
</template>
|
||||
<template v-else-if="act.action.by && action.action_by">
|
||||
{{ $t('private_user') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('external') }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="date" v-tooltip.start="new Date(act.action_on)">
|
||||
{{ act.date_relative }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<template v-if="act.comment">
|
||||
<span v-html="marked(act.comment)" />
|
||||
</template>
|
||||
<template v-else-if="act.action === 'create'">
|
||||
{{ $t('activity_delta_created') }}
|
||||
</template>
|
||||
<template v-else-if="act.action === 'update'">
|
||||
{{ $t('activity_delta_updated') }}
|
||||
</template>
|
||||
<template v-else-if="act.action === 'delete'">
|
||||
{{ $t('activity_delta_deleted') }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
|
||||
<div class="activity-record">
|
||||
<div class="content">{{ $t('activity_delta_created_externally') }}</div>
|
||||
</div>
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import api from '@/api';
|
||||
import localizedFormatDistance from '@/utils/localized-format-distance';
|
||||
import marked from 'marked';
|
||||
import { Avatar } from '@/stores/user/types';
|
||||
import notify from '@/utils/notify';
|
||||
import i18n from '@/lang';
|
||||
|
||||
type Activity = {
|
||||
action: 'create' | 'update' | 'delete' | 'comment';
|
||||
action_by: null | {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
avatar: null | Avatar;
|
||||
};
|
||||
action_on: string;
|
||||
edited_on: null | string;
|
||||
comment: null | string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { activity, loading, error, refresh } = useActivity(
|
||||
props.collection,
|
||||
props.primaryKey
|
||||
);
|
||||
const { newCommentContent, postComment, saving } = useComment();
|
||||
|
||||
return { activity, loading, error, marked, newCommentContent, postComment, saving };
|
||||
|
||||
function useActivity(collection: string, primaryKey: string | number) {
|
||||
const activity = ref<Activity[]>(null);
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
getActivity();
|
||||
|
||||
return { activity, error, loading, refresh };
|
||||
|
||||
async function getActivity() {
|
||||
error.value = null;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${projectsStore.state.currentProjectKey}/activity`,
|
||||
{
|
||||
params: {
|
||||
'filter[collection][eq]': collection,
|
||||
'filter[item][eq]': primaryKey,
|
||||
'filter[action][in]': 'comment,create,update,delete',
|
||||
sort: '-id', // directus_activity has auto increment and is therefore in chronological order
|
||||
fields: [
|
||||
'id',
|
||||
'action',
|
||||
'action_on',
|
||||
'action_by.id',
|
||||
'action_by.first_name',
|
||||
'action_by.last_name',
|
||||
'action_by.avatar.data',
|
||||
'comment',
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const records = [];
|
||||
|
||||
for (const record of response.data.data) {
|
||||
records.push({
|
||||
...record,
|
||||
date_relative: await localizedFormatDistance(
|
||||
new Date(record.action_on),
|
||||
new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
activity.value = records;
|
||||
} catch (error) {
|
||||
error.value = error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await getActivity();
|
||||
}
|
||||
}
|
||||
|
||||
function useComment() {
|
||||
const newCommentContent = ref(null);
|
||||
const saving = ref(false);
|
||||
|
||||
return { newCommentContent, postComment, saving };
|
||||
|
||||
async function postComment() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await api.post(`/${projectsStore.state.currentProjectKey}/activity/comment`, {
|
||||
collection: props.collection,
|
||||
item: props.primaryKey,
|
||||
comment: newCommentContent.value,
|
||||
});
|
||||
|
||||
await refresh();
|
||||
|
||||
newCommentContent.value = null;
|
||||
|
||||
notify({
|
||||
title: i18n.t('post_comment_success'),
|
||||
type: 'success',
|
||||
});
|
||||
} catch {
|
||||
notify({
|
||||
title: i18n.t('post_comment_failed'),
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.post-comment {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
}
|
||||
|
||||
.activity-record {
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 2px solid var(--border-normal);
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.v-avatar {
|
||||
--v-avatar-color: var(--background-normal-alt);
|
||||
|
||||
margin-right: 8px;
|
||||
|
||||
.v-icon {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
}
|
||||
}
|
||||
|
||||
.name,
|
||||
.date {
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all var(--slow) var(--transition);
|
||||
}
|
||||
|
||||
.slide-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.slide-move {
|
||||
transition: all 500ms var(--transition);
|
||||
}
|
||||
|
||||
.slide-enter,
|
||||
.slide-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +0,0 @@
|
||||
import ActivityDrawerDetail from './activity-drawer-detail.vue';
|
||||
|
||||
export { ActivityDrawerDetail };
|
||||
export default ActivityDrawerDetail;
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<v-textarea
|
||||
class="new-comment"
|
||||
:placeholder="$t('leave_comment')"
|
||||
v-model="newCommentContent"
|
||||
expand-on-focus
|
||||
>
|
||||
<template #append>
|
||||
<v-icon name="alternate_email" class="add-mention" />
|
||||
<v-icon name="insert_emoticon" class="add-emoji" />
|
||||
<v-button
|
||||
:disabled="!newCommentContent || newCommentContent.length === 0"
|
||||
:loading="saving"
|
||||
class="post-comment"
|
||||
@click="postComment"
|
||||
x-small
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</v-button>
|
||||
</template>
|
||||
</v-textarea>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import notify from '@/utils/notify';
|
||||
import api from '@/api';
|
||||
import i18n from '@/lang';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
refresh: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const newCommentContent = ref(null);
|
||||
const saving = ref(false);
|
||||
|
||||
return { newCommentContent, postComment, saving };
|
||||
|
||||
async function postComment() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await api.post(`/${projectsStore.state.currentProjectKey}/activity/comment`, {
|
||||
collection: props.collection,
|
||||
item: props.primaryKey,
|
||||
comment: newCommentContent.value,
|
||||
});
|
||||
|
||||
await props.refresh();
|
||||
|
||||
newCommentContent.value = null;
|
||||
|
||||
notify({
|
||||
title: i18n.t('post_comment_success'),
|
||||
type: 'success',
|
||||
});
|
||||
} catch {
|
||||
notify({
|
||||
title: i18n.t('post_comment_failed'),
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.new-comment {
|
||||
&::v-deep {
|
||||
&.expand-on-focus {
|
||||
textarea {
|
||||
position: relative;
|
||||
transition: margin-bottom var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
.append {
|
||||
&::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 36px;
|
||||
left: 0;
|
||||
height: 8px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--background-page-rgb), 0) 0%,
|
||||
rgba(var(--background-page-rgb), 1) 100%
|
||||
);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-within,
|
||||
&.has-content {
|
||||
textarea {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-mention {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
color: var(--foreground-subdued);
|
||||
cursor: pointer;
|
||||
transition: color var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
.add-emoji {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 36px;
|
||||
color: var(--foreground-subdued);
|
||||
cursor: pointer;
|
||||
transition: color var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
.add-mention,
|
||||
.add-emoji {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.post-comment {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="comment-header">
|
||||
<v-avatar x-small>
|
||||
<img
|
||||
v-if="avatarSource"
|
||||
:src="avatarSource"
|
||||
:alt="activity.action_by.first_name + ' ' + activity.action_by.last_name"
|
||||
/>
|
||||
<v-icon v-else name="person_outline" />
|
||||
</v-avatar>
|
||||
|
||||
<div class="name">
|
||||
<template v-if="activity.action_by && activity.action_by">
|
||||
{{ activity.action_by.first_name }} {{ activity.action_by.last_name }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="activity.action_by && action.action_by">
|
||||
{{ $t('private_user') }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<v-menu show-arrow placement="bottom-end" close-on-content-click>
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-icon class="more" :class="{ active }" name="more_horiz" @click="toggle" />
|
||||
<div class="time">
|
||||
<span
|
||||
class="dot"
|
||||
v-if="activity.edited_on !== null"
|
||||
v-tooltip="editedOnFormatted"
|
||||
/>
|
||||
{{ formattedTime }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
<v-list-item @click="$emit('edit')">
|
||||
<v-list-item-icon><v-icon name="edit" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('edit') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item @click="confirmDelete = true">
|
||||
<v-list-item-icon><v-icon name="delete" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('delete') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('delete_comment') }}</v-card-title>
|
||||
<v-card-text>{{ $t('delete_are_you_sure') }}</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button @click="confirmDelete = false" secondary>
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button @click="remove" class="action-delete" :loading="deleting">
|
||||
{{ $t('delete') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed, ref, watch } from '@vue/composition-api';
|
||||
import { Activity } from './types';
|
||||
import format from 'date-fns/format';
|
||||
import i18n from '@/lang';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import api from '@/api';
|
||||
import localizedFormat from '@/utils/localized-format';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
activity: {
|
||||
type: Object as PropType<Activity>,
|
||||
required: true,
|
||||
},
|
||||
refresh: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const editedOnFormatted = ref('');
|
||||
|
||||
watch(
|
||||
() => props.activity,
|
||||
async () => {
|
||||
if (props.activity.edited_on) {
|
||||
editedOnFormatted.value = await localizedFormat(
|
||||
new Date(props.activity.edited_on),
|
||||
String(i18n.t('date-fns_datetime'))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
if (props.activity.action_on) {
|
||||
// action_on is in iso-8601
|
||||
return format(new Date(props.activity.action_on), String(i18n.t('date-fns_time')));
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const avatarSource = computed(() => {
|
||||
if (!props.activity.action_by?.avatar) return null;
|
||||
|
||||
return (
|
||||
props.activity.action_by.avatar.data.thumbnails.find(
|
||||
(thumb) => thumb.key === 'directus-small-crop'
|
||||
)?.url || null
|
||||
);
|
||||
});
|
||||
|
||||
const { confirmDelete, deleting, remove } = useDelete();
|
||||
|
||||
return { formattedTime, avatarSource, confirmDelete, deleting, remove, editedOnFormatted };
|
||||
|
||||
function useDelete() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
const confirmDelete = ref(false);
|
||||
const deleting = ref(false);
|
||||
|
||||
return { confirmDelete, deleting, remove };
|
||||
|
||||
async function remove() {
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
await api.delete(`/${currentProjectKey}/activity/comment/${props.activity.id}`);
|
||||
await props.refresh();
|
||||
confirmDelete.value = false;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.v-avatar {
|
||||
--v-avatar-color: var(--background-normal-alt);
|
||||
|
||||
flex-basis: 24px;
|
||||
margin-right: 8px;
|
||||
|
||||
.v-icon {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
margin-right: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
position: relative;
|
||||
flex-basis: 24px;
|
||||
color: var(--foreground-subdued);
|
||||
|
||||
.more {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all var(--slow) var(--transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
text-transform: lowercase;
|
||||
opacity: 1;
|
||||
transition: opacity var(--slow) var(--transition);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.more.active + .time {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-delete {
|
||||
--v-button-background-color: var(--danger-25);
|
||||
--v-button-color: var(--danger);
|
||||
--v-button-background-color-hover: var(--danger-50);
|
||||
--v-button-color-hover: var(--danger);
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
background-color: var(--warning);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="comment-item">
|
||||
<comment-item-header :refresh="refresh" :activity="activity" @edit="editing = true" />
|
||||
|
||||
<v-textarea v-if="editing" v-model="edits">
|
||||
<template #append>
|
||||
<v-button :loading="savingEdits" class="post-comment" @click="saveEdits" x-small>
|
||||
{{ $t('save') }}
|
||||
</v-button>
|
||||
</template>
|
||||
</v-textarea>
|
||||
|
||||
<div v-else class="content">
|
||||
<span v-html="htmlContent" />
|
||||
|
||||
<!-- @TODO: Dynamically add element below if the comment overflows -->
|
||||
<!-- <div v-if="activity.id == 204" class="expand-text">
|
||||
<span>{{ $t('click_to_expand') }}</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, computed, watch } from '@vue/composition-api';
|
||||
import { Activity } from './types';
|
||||
import CommentItemHeader from './comment-item-header.vue';
|
||||
import marked from 'marked';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import api from '@/api';
|
||||
|
||||
export default defineComponent({
|
||||
components: { CommentItemHeader },
|
||||
props: {
|
||||
activity: {
|
||||
type: Object as PropType<Activity>,
|
||||
required: true,
|
||||
},
|
||||
refresh: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const htmlContent = computed(() =>
|
||||
props.activity.comment ? marked(props.activity.comment) : null
|
||||
);
|
||||
|
||||
const { edits, editing, savingEdits, saveEdits } = useEdits();
|
||||
|
||||
return { htmlContent, edits, editing, savingEdits, saveEdits };
|
||||
|
||||
function useEdits() {
|
||||
const edits = ref(props.activity.comment);
|
||||
const editing = ref(false);
|
||||
const savingEdits = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.activity,
|
||||
() => (edits.value = props.activity.comment)
|
||||
);
|
||||
|
||||
return { edits, editing, savingEdits, saveEdits };
|
||||
|
||||
async function saveEdits() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
savingEdits.value = true;
|
||||
|
||||
try {
|
||||
await api.patch(`/${currentProjectKey}/activity/comment/${props.activity.id}`, {
|
||||
comment: edits.value,
|
||||
});
|
||||
await props.refresh();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
savingEdits.value = false;
|
||||
editing.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.comment-item {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
padding: 8px;
|
||||
background-color: var(--background-page);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.content {
|
||||
max-height: 300px;
|
||||
overflow-y: hidden;
|
||||
|
||||
::v-deep {
|
||||
a {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 8px 0;
|
||||
padding-left: 6px;
|
||||
color: var(--foreground-subdued);
|
||||
font-style: italic;
|
||||
border-left: 2px solid var(--border-normal);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 2px;
|
||||
margin: 12px 0;
|
||||
border: 0;
|
||||
border-top: 2px solid var(--border-normal);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.expand {
|
||||
.content {
|
||||
&::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 4px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
height: 40px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--background-page-rgb), 0) 0%,
|
||||
rgba(var(--background-page-rgb), 0.8) 25%,
|
||||
rgba(var(--background-page-rgb), 1) 100%
|
||||
);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.expand-text {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 8px;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
padding: 4px 12px 5px;
|
||||
color: var(--foreground-subdued);
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
background-color: var(--background-normal);
|
||||
border-radius: 12px;
|
||||
transition: color var(--fast) var(--transition),
|
||||
background-color var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
&:hover span {
|
||||
color: var(--foreground-inverted);
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
::v-deep .comment-header {
|
||||
.header-right {
|
||||
.time {
|
||||
opacity: 0;
|
||||
}
|
||||
.more {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-comment {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<drawer-detail :title="$t('comments')" icon="chat_bubble_outline">
|
||||
<comment-input :refresh="refresh" :collection="collection" :primary-key="primaryKey" />
|
||||
|
||||
<v-progress-linear indeterminate v-if="loading" />
|
||||
|
||||
<div v-else-if="!activity || activity.length === 0" class="empty">
|
||||
<div class="content">{{ $t('no_comments') }}</div>
|
||||
</div>
|
||||
|
||||
<template v-else v-for="group in activity">
|
||||
<v-divider :key="group.date.toString()">{{ group.dateFormatted }}</v-divider>
|
||||
|
||||
<template v-for="item in group.activity">
|
||||
<comment-item :refresh="refresh" :activity="item" :key="item.id" />
|
||||
</template>
|
||||
</template>
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import api from '@/api';
|
||||
import { Activity, ActivityByDate } from './types';
|
||||
import CommentInput from './comment-input.vue';
|
||||
import { groupBy } from 'lodash';
|
||||
import i18n from '@/lang';
|
||||
import formatLocalized from '@/utils/localized-format';
|
||||
import { isToday, isYesterday, isThisYear } from 'date-fns';
|
||||
import { TranslateResult } from 'vue-i18n';
|
||||
import CommentItem from './comment-item.vue';
|
||||
import { orderBy } from 'lodash';
|
||||
|
||||
export default defineComponent({
|
||||
components: { CommentInput, CommentItem },
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { activity, loading, error, refresh } = useActivity(
|
||||
props.collection,
|
||||
props.primaryKey
|
||||
);
|
||||
|
||||
return {
|
||||
activity,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
};
|
||||
|
||||
function useActivity(collection: string, primaryKey: string | number) {
|
||||
const activity = ref<ActivityByDate[]>(null);
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
getActivity();
|
||||
|
||||
return { activity, error, loading, refresh };
|
||||
|
||||
async function getActivity() {
|
||||
error.value = null;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${projectsStore.state.currentProjectKey}/activity`,
|
||||
{
|
||||
params: {
|
||||
'filter[collection][eq]': collection,
|
||||
'filter[item][eq]': primaryKey,
|
||||
'filter[action][in]': 'comment',
|
||||
'filter[comment_deleted_on][null]': 1,
|
||||
sort: '-id', // directus_activity has auto increment and is therefore in chronological order
|
||||
fields: [
|
||||
'id',
|
||||
'action',
|
||||
'action_on',
|
||||
'action_by.id',
|
||||
'action_by.first_name',
|
||||
'action_by.last_name',
|
||||
'action_by.avatar.data',
|
||||
'comment',
|
||||
'edited_on',
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const activityByDate = groupBy(response.data.data, (activity: Activity) => {
|
||||
// activity's action_on date is in iso-8601
|
||||
const date = new Date(new Date(activity.action_on).toDateString());
|
||||
return date;
|
||||
});
|
||||
|
||||
const activityGrouped: ActivityByDate[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(activityByDate)) {
|
||||
const date = new Date(key);
|
||||
const today = isToday(date);
|
||||
const yesterday = isYesterday(date);
|
||||
const thisYear = isThisYear(date);
|
||||
|
||||
let dateFormatted: TranslateResult;
|
||||
|
||||
if (today) dateFormatted = i18n.t('today');
|
||||
else if (yesterday) dateFormatted = i18n.t('yesterday');
|
||||
else if (thisYear)
|
||||
dateFormatted = await formatLocalized(
|
||||
date,
|
||||
String(i18n.t('date-fns_date_short_no_year'))
|
||||
);
|
||||
else
|
||||
dateFormatted = await formatLocalized(
|
||||
date,
|
||||
String(i18n.t('date-fns_date_short'))
|
||||
);
|
||||
|
||||
activityGrouped.push({
|
||||
date: date,
|
||||
dateFormatted: String(dateFormatted),
|
||||
activity: value,
|
||||
});
|
||||
}
|
||||
|
||||
activity.value = orderBy(activityGrouped, ['date'], ['desc']);
|
||||
} catch (error) {
|
||||
error.value = error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await getActivity();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-progress-linear {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.v-divider {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
background-color: var(--background-normal);
|
||||
box-shadow: 0 0 4px 2px var(--background-normal);
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--foreground-subdued);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import CommentsDrawerDetail from './comments-drawer-detail.vue';
|
||||
|
||||
export { CommentsDrawerDetail };
|
||||
export default CommentsDrawerDetail;
|
||||
@@ -0,0 +1,9 @@
|
||||
# Comments Drawer
|
||||
|
||||
Renders an comment timeline in a drawer section meant to be used in the drawer sidebar.
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<comments-drawer-detail collection="authors" primary-key="15" />
|
||||
```
|
||||
21
src/views/private/components/comments-drawer-detail/types.ts
Normal file
21
src/views/private/components/comments-drawer-detail/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Avatar } from '@/stores/user/types';
|
||||
|
||||
export type Activity = {
|
||||
id: number;
|
||||
action: 'comment';
|
||||
action_by: null | {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
avatar: null | Avatar;
|
||||
};
|
||||
action_on: string;
|
||||
edited_on: null | string;
|
||||
comment: null | string;
|
||||
};
|
||||
|
||||
export type ActivityByDate = {
|
||||
date: Date;
|
||||
dateFormatted: string;
|
||||
activity: Activity[];
|
||||
};
|
||||
@@ -95,7 +95,7 @@ body {
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
@click="_active = false"
|
||||
/>
|
||||
|
||||
<v-button class="close" @click="_active = false" icon rounded secondary>
|
||||
<v-button class="close" @click="_active = false" icon rounded>
|
||||
<v-icon name="close" />
|
||||
</v-button>
|
||||
</v-dialog>
|
||||
@@ -117,6 +117,11 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.close {
|
||||
--v-button-background-color: var(--white);
|
||||
--v-button-color: var(--foreground-subdued);
|
||||
--v-button-background-color-hover: var(--white);
|
||||
--v-button-color-hover: var(--foreground-normal);
|
||||
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
right: 32px;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="file-preview" v-if="type">
|
||||
<div v-if="type === 'image'" class="image" :class="{ svg: isSVG }" @click="$emit('click')">
|
||||
<img :src="src" :width="width" :height="height" :alt="title" />
|
||||
<v-icon name="fullscreen" />
|
||||
</div>
|
||||
|
||||
<video v-else-if="type === 'video'" controls :src="src" />
|
||||
@@ -71,17 +72,6 @@ export default defineComponent({
|
||||
margin-bottom: var(--form-vertical-gap);
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.svg {
|
||||
padding: 64px;
|
||||
background-color: var(--background-subdued);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
img,
|
||||
video,
|
||||
audio {
|
||||
@@ -91,4 +81,38 @@ audio {
|
||||
object-fit: contain;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
z-index: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
z-index: 2;
|
||||
color: white;
|
||||
text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
transition: opacity var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.v-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.svg {
|
||||
padding: 64px;
|
||||
background-color: var(--background-subdued);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<header class="header-bar" ref="headerEl" :class="{ shadow: showShadow }">
|
||||
<header class="header-bar" ref="headerEl" :class="{ collapsed: collapsed }">
|
||||
<v-button secondary class="nav-toggle" icon rounded @click="$emit('toggle:nav')">
|
||||
<v-icon name="menu" />
|
||||
</v-button>
|
||||
@@ -9,7 +9,9 @@
|
||||
</div>
|
||||
|
||||
<div class="title-container">
|
||||
<slot name="headline" />
|
||||
<div class="headline">
|
||||
<slot name="headline" />
|
||||
</div>
|
||||
<div class="title">
|
||||
<slot name="title">
|
||||
<slot name="title:prepend" />
|
||||
@@ -46,11 +48,11 @@ export default defineComponent({
|
||||
setup() {
|
||||
const headerEl = ref<Element>();
|
||||
|
||||
const showShadow = ref(false);
|
||||
const collapsed = ref(false);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) => {
|
||||
showShadow.value = e.intersectionRatio < 1;
|
||||
collapsed.value = e.intersectionRatio < 1;
|
||||
},
|
||||
{ threshold: [1] }
|
||||
);
|
||||
@@ -63,7 +65,7 @@ export default defineComponent({
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
return { headerEl, showShadow };
|
||||
return { headerEl, collapsed };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -87,10 +89,6 @@ export default defineComponent({
|
||||
box-shadow: 0;
|
||||
transition: box-shadow var(--medium) var(--transition);
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 4px 7px -4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
@include breakpoint(medium) {
|
||||
display: none;
|
||||
@@ -106,8 +104,17 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.title-container {
|
||||
position: relative;
|
||||
margin-left: 12px;
|
||||
|
||||
.headline {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
transition: opacity var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -119,6 +126,17 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
box-shadow: 0 4px 7px -4px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.title-container {
|
||||
.headline {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default defineComponent({
|
||||
right: 8px;
|
||||
left: 8px;
|
||||
z-index: 50;
|
||||
width: 280px;
|
||||
width: 260px;
|
||||
direction: rtl;
|
||||
|
||||
> *,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import RevisionsDrawerDetail from './revisions-drawer-detail.vue';
|
||||
|
||||
export { RevisionsDrawerDetail };
|
||||
export default RevisionsDrawerDetail;
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<drawer-detail :title="$t('revisions')" icon="change_history">
|
||||
<!-- "Today", "Yesterday", and then: "Sep 6", "Sep 6, 2019" (previous years) -->
|
||||
<div class="day-break"><span>Today</span></div>
|
||||
|
||||
<transition-group name="slide" tag="div">
|
||||
<div v-for="act in activity" :key="act.id" class="revision internal">
|
||||
<div class="header">
|
||||
<template v-if="act.action === 'create'">
|
||||
<span class="dot create"></span>
|
||||
{{ $t('revision_delta_created') }}
|
||||
</template>
|
||||
<template v-else-if="act.action === 'update'">
|
||||
<span class="dot update"></span>
|
||||
{{ $t('revision_delta_updated', { count: 3 }) }}
|
||||
</template>
|
||||
<template v-else-if="act.action === 'soft-delete'">
|
||||
<span class="dot delete"></span>
|
||||
{{ $t('revision_delta_soft_deleted') }}
|
||||
</template>
|
||||
<template v-else-if="act.action === 'delete'">
|
||||
<span class="dot delete"></span>
|
||||
{{ $t('revision_delta_deleted') }}
|
||||
</template>
|
||||
|
||||
<v-icon name="expand_more" class="more" />
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<span class="time">{{ getFormattedTime(act.action_on) }}</span>
|
||||
—
|
||||
<span class="name">
|
||||
<template v-if="act.action_by && act.action_by">
|
||||
{{ act.action_by.first_name }} {{ act.action_by.last_name }}
|
||||
</template>
|
||||
<template v-else-if="act.action_by && action.action_by">
|
||||
{{ $t('private_user') }}
|
||||
</template>
|
||||
</span>
|
||||
|
||||
<div v-if="act.comment" class="comment" v-html="marked(act.comment)" />
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
|
||||
<!-- Only show this if "created" revision doesn't exist for this item -->
|
||||
<div class="revision external">
|
||||
<div class="header">
|
||||
<span class="dot"></span>
|
||||
{{ $t('revision_delta_created_externally') }}
|
||||
</div>
|
||||
<div class="content">{{ $t('revision_unknown') }}</div>
|
||||
</div>
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import api from '@/api';
|
||||
import localizedFormatDistance from '@/utils/localized-format-distance';
|
||||
import marked from 'marked';
|
||||
import { Avatar } from '@/stores/user/types';
|
||||
import notify from '@/utils/notify';
|
||||
import format from 'date-fns/format';
|
||||
import i18n from '@/lang';
|
||||
|
||||
type Activity = {
|
||||
action: 'create' | 'update' | 'soft-delete' | 'delete';
|
||||
action_by: null | {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
avatar: null | Avatar;
|
||||
};
|
||||
action_on: string;
|
||||
edited_on: null | string;
|
||||
comment: null | string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { activity, loading, error, refresh } = useActivity(
|
||||
props.collection,
|
||||
props.primaryKey
|
||||
);
|
||||
const { newCommentContent, postComment, saving } = useComment();
|
||||
|
||||
return {
|
||||
activity,
|
||||
loading,
|
||||
error,
|
||||
marked,
|
||||
newCommentContent,
|
||||
postComment,
|
||||
saving,
|
||||
getFormattedTime,
|
||||
};
|
||||
|
||||
function getFormattedTime(datetime: string) {
|
||||
return format(new Date(datetime), String(i18n.t('date-fns_time')));
|
||||
}
|
||||
|
||||
function useActivity(collection: string, primaryKey: string | number) {
|
||||
const activity = ref<Activity[]>(null);
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
getActivity();
|
||||
|
||||
return { activity, error, loading, refresh };
|
||||
|
||||
async function getActivity() {
|
||||
error.value = null;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${projectsStore.state.currentProjectKey}/activity`,
|
||||
{
|
||||
params: {
|
||||
'filter[collection][eq]': collection,
|
||||
'filter[item][eq]': primaryKey,
|
||||
'filter[action][in]': 'create,update,soft-delete,delete',
|
||||
sort: '-id', // directus_activity has auto increment and is therefore in chronological order
|
||||
fields: [
|
||||
'id',
|
||||
'action',
|
||||
'action_on',
|
||||
'action_by.id',
|
||||
'action_by.first_name',
|
||||
'action_by.last_name',
|
||||
'action_by.avatar.data',
|
||||
'comment',
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const records = [];
|
||||
|
||||
for (const record of response.data.data) {
|
||||
records.push({
|
||||
...record,
|
||||
date_relative: await localizedFormatDistance(
|
||||
new Date(record.action_on),
|
||||
new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
activity.value = records;
|
||||
} catch (error) {
|
||||
error.value = error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await getActivity();
|
||||
}
|
||||
}
|
||||
|
||||
function useComment() {
|
||||
const newCommentContent = ref(null);
|
||||
const saving = ref(false);
|
||||
|
||||
return { newCommentContent, postComment, saving };
|
||||
|
||||
async function postComment() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await api.post(`/${projectsStore.state.currentProjectKey}/activity/comment`, {
|
||||
collection: props.collection,
|
||||
item: props.primaryKey,
|
||||
comment: newCommentContent.value,
|
||||
});
|
||||
|
||||
await refresh();
|
||||
|
||||
newCommentContent.value = null;
|
||||
|
||||
notify({
|
||||
title: i18n.t('post_comment_success'),
|
||||
type: 'success',
|
||||
});
|
||||
} catch {
|
||||
notify({
|
||||
title: i18n.t('post_comment_failed'),
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.day-break {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
background-color: var(--background-normal);
|
||||
box-shadow: 0 0 4px 2px var(--background-normal);
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
z-index: 2;
|
||||
padding-right: 8px;
|
||||
color: var(--foreground-subdued);
|
||||
background-color: var(--background-normal);
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
height: 2px;
|
||||
background-color: var(--border-normal);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.revision {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
margin-left: 20px;
|
||||
|
||||
&.internal {
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: -17px;
|
||||
z-index: 1;
|
||||
width: 2px;
|
||||
height: calc(100% + 12px);
|
||||
background-color: var(--background-normal-alt);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&.external {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: -22px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: var(--warning);
|
||||
border: 2px solid var(--background-normal);
|
||||
border-radius: 8px;
|
||||
|
||||
&.create {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
&.update {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
&.delete {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
flex-basis: 24px;
|
||||
color: var(--foreground-subdued);
|
||||
transition: color var(--fast) var(--transition);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--foreground-subdued);
|
||||
line-height: 16px;
|
||||
|
||||
.time {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.comment {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background-color: var(--background-page);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 20px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: var(--background-page);
|
||||
border-radius: 2px;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.header {
|
||||
.more {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all var(--slow) var(--transition);
|
||||
}
|
||||
|
||||
.slide-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.slide-move {
|
||||
transition: all 500ms var(--transition);
|
||||
}
|
||||
|
||||
.slide-enter,
|
||||
.slide-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user