Migrate to Vue 3 and Vite
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
153
js/src/components/Resource/DraggableList.vue
Normal file
153
js/src/components/Resource/DraggableList.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<section>
|
||||
<p v-if="isRoot">
|
||||
{{ $t("A place to store links to documents or resources of any type.") }}
|
||||
</p>
|
||||
<div class="list-header">
|
||||
<div class="list-header-right">
|
||||
<o-checkbox v-model="checkedAll" v-if="resources.length > 0" />
|
||||
<div class="actions" v-if="validCheckedResources.length > 0">
|
||||
<small>
|
||||
{{
|
||||
$tc("No resources selected", validCheckedResources.length, {
|
||||
count: validCheckedResources.length,
|
||||
})
|
||||
}}
|
||||
</small>
|
||||
<o-button
|
||||
variant="danger"
|
||||
icon-right="delete"
|
||||
size="small"
|
||||
@click="deleteMultipleResources"
|
||||
>{{ $t("Delete") }}</o-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
:list="resources"
|
||||
:sort="false"
|
||||
:group="groupObject"
|
||||
v-if="resources.length > 0"
|
||||
tag="transition-group"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="resource-item">
|
||||
<div
|
||||
class="resource-checkbox px-2"
|
||||
:class="{ checked: checkedResources[element.id as string] }"
|
||||
>
|
||||
<o-checkbox v-model="checkedResources[element.id as string]" />
|
||||
</div>
|
||||
<resource-item
|
||||
:resource="element"
|
||||
v-if="element.type !== 'folder'"
|
||||
@delete="emit('delete', $event)"
|
||||
@rename="emit('rename', $event)"
|
||||
@move="emit('move', $event)"
|
||||
/>
|
||||
<folder-item
|
||||
:resource="element"
|
||||
:group="group"
|
||||
@delete="emit('delete', $event)"
|
||||
@rename="emit('rename', $event)"
|
||||
@move="emit('move', $event)"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
class="content has-text-centered has-text-grey"
|
||||
v-if="resources.length === 0"
|
||||
>
|
||||
<p>{{ $t("No resources in this folder") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import ResourceItem from "@/components/Resource/ResourceItem.vue";
|
||||
import FolderItem from "@/components/Resource/FolderItem.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { IResource } from "@/types/resource";
|
||||
import Draggable from "@xiaoshuapp/draggable";
|
||||
import { IGroup } from "@/types/actor";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ resources: IResource[]; isRoot: boolean; group: IGroup }>(),
|
||||
{ isRoot: false }
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "move", resource: IResource): void;
|
||||
(e: "rename", resource: IResource): void;
|
||||
(e: "delete", resourceID: string): void;
|
||||
}>();
|
||||
|
||||
const groupObject: Record<string, unknown> = {
|
||||
name: "resources",
|
||||
pull: "clone",
|
||||
put: true,
|
||||
};
|
||||
|
||||
const checkedResources = ref<{ [key: string]: boolean }>({});
|
||||
|
||||
const validCheckedResources = ref<string[]>([]);
|
||||
|
||||
const checkedAll = ref(false);
|
||||
|
||||
watch(checkedResources, () => {
|
||||
const newValidCheckedResources: string[] = [];
|
||||
Object.entries(checkedResources).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
newValidCheckedResources.push(key);
|
||||
}
|
||||
});
|
||||
validCheckedResources.value = newValidCheckedResources;
|
||||
});
|
||||
|
||||
const deleteMultipleResources = async (): Promise<void> => {
|
||||
validCheckedResources.value.forEach((resourceID) => {
|
||||
emit("delete", resourceID);
|
||||
});
|
||||
};
|
||||
|
||||
watch(checkedAll, () => {
|
||||
props.resources.forEach(({ id }) => {
|
||||
if (!id) return;
|
||||
checkedResources.value[id] = checkedAll.value;
|
||||
});
|
||||
});
|
||||
|
||||
const deleteResource = (resourceID: string) => {
|
||||
validCheckedResources.value = validCheckedResources.value.filter(
|
||||
(id) => id !== resourceID
|
||||
);
|
||||
delete checkedResources.value[resourceID];
|
||||
emit("delete", resourceID);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/_mixins" as *;
|
||||
.resource-item,
|
||||
.new-resource-preview {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
border: 1px solid #c0cdd9;
|
||||
border-radius: 4px;
|
||||
// color: #444b5d;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
.resource-checkbox {
|
||||
align-self: center;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&:hover .resource-checkbox,
|
||||
.resource-checkbox.checked {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,122 +4,117 @@
|
||||
:to="{
|
||||
name: RouteName.RESOURCE_FOLDER,
|
||||
params: {
|
||||
path: ResourceMixin.resourcePathArray(resource),
|
||||
path: resourcePathArray(resource),
|
||||
preferredUsername: usernameWithDomain(group),
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div class="preview">
|
||||
<b-icon icon="folder" size="is-large" />
|
||||
<Folder :size="48" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<h3>{{ resource.title }}</h3>
|
||||
<span class="host" v-if="inline">{{
|
||||
resource.updatedAt | formatDateTimeString
|
||||
<span class="host" v-if="inline && resource.updatedAt">{{
|
||||
formatDateTimeString(resource.updatedAt?.toString())
|
||||
}}</span>
|
||||
</div>
|
||||
<draggable
|
||||
<!-- <draggable
|
||||
v-if="!inline"
|
||||
class="dropzone"
|
||||
v-model="list"
|
||||
itemKey="id"
|
||||
:sort="false"
|
||||
:group="groupObject"
|
||||
@change="onChange"
|
||||
/>
|
||||
/> -->
|
||||
</router-link>
|
||||
<resource-dropdown
|
||||
class="actions"
|
||||
v-if="!inline"
|
||||
@delete="$emit('delete', resource.id)"
|
||||
@move="$emit('move', resource)"
|
||||
@rename="$emit('rename', resource)"
|
||||
@delete="emit('delete', resource.id as string)"
|
||||
@move="emit('move', resource)"
|
||||
@rename="emit('rename', resource)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import Draggable, { ChangeEvent } from "vuedraggable";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { IResource } from "../../types/resource";
|
||||
import RouteName from "../../router/name";
|
||||
import ResourceMixin from "../../mixins/resource";
|
||||
import { IGroup, usernameWithDomain } from "../../types/actor";
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import Draggable, { ChangeEvent } from "@xiaoshuapp/draggable";
|
||||
// import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { IResource } from "@/types/resource";
|
||||
import RouteName from "@/router/name";
|
||||
import { IGroup, usernameWithDomain } from "@/types/actor";
|
||||
import ResourceDropdown from "./ResourceDropdown.vue";
|
||||
import { UPDATE_RESOURCE } from "../../graphql/resources";
|
||||
import { UPDATE_RESOURCE } from "@/graphql/resources";
|
||||
import { ref } from "vue";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { resourcePathArray } from "@/components/Resource/utils";
|
||||
import Folder from "vue-material-design-icons/Folder.vue";
|
||||
|
||||
@Component({
|
||||
components: { Draggable, ResourceDropdown },
|
||||
})
|
||||
export default class FolderItem extends Mixins(ResourceMixin) {
|
||||
@Prop({ required: true, type: Object }) resource!: IResource;
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
resource: IResource;
|
||||
group: IGroup;
|
||||
inline?: boolean;
|
||||
}>(),
|
||||
{ inline: false }
|
||||
);
|
||||
|
||||
@Prop({ required: true, type: Object }) group!: IGroup;
|
||||
const emit = defineEmits<{
|
||||
(e: "move", resource: IResource): void;
|
||||
(e: "rename", resource: IResource): void;
|
||||
(e: "delete", resourceID: string): void;
|
||||
}>();
|
||||
|
||||
@Prop({ required: false, default: false }) inline!: boolean;
|
||||
const list = ref([]);
|
||||
|
||||
list = [];
|
||||
const groupObject: Record<string, unknown> = {
|
||||
name: `folder-${props.resource?.title}`,
|
||||
pull: false,
|
||||
put: ["resources"],
|
||||
};
|
||||
|
||||
groupObject: Record<string, unknown> = {
|
||||
name: `folder-${this.resource?.title}`,
|
||||
pull: false,
|
||||
put: ["resources"],
|
||||
};
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
ResourceMixin = ResourceMixin;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
async onChange(evt: ChangeEvent<IResource>): Promise<Route | undefined> {
|
||||
if (evt.added && evt.added.element) {
|
||||
const movedResource = evt.added.element as IResource;
|
||||
const updatedResource = await this.moveResource(movedResource);
|
||||
if (updatedResource && this.resource.path) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
return this.$router.push({
|
||||
name: RouteName.RESOURCE_FOLDER,
|
||||
params: {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
path: ResourceMixin.resourcePathArray(this.resource),
|
||||
preferredUsername: this.group.preferredUsername,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
const onChange = async (evt: ChangeEvent<IResource>) => {
|
||||
if (evt.added && evt.added.element) {
|
||||
const movedResource = evt.added.element as IResource;
|
||||
moveResource({
|
||||
id: props.resource.id,
|
||||
path: `${props.resource.path}/${props.resource.title}`,
|
||||
parentId: props.resource.id,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
async moveResource(resource: IResource): Promise<IResource | undefined> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>(
|
||||
{
|
||||
mutation: UPDATE_RESOURCE,
|
||||
variables: {
|
||||
id: resource.id,
|
||||
path: `${this.resource.path}/${resource.title}`,
|
||||
parentId: this.resource.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!data) {
|
||||
console.error("Error while updating resource");
|
||||
return undefined;
|
||||
}
|
||||
return data.updateResource;
|
||||
} catch (e: any) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
const {
|
||||
mutate: moveResource,
|
||||
onDone: onMovedResource,
|
||||
onError: onMovedResourceError,
|
||||
} = useMutation<{ updateResource: IResource }>(UPDATE_RESOURCE);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
onMovedResource(({ data }) => {
|
||||
if (data?.updateResource && props.resource.path) {
|
||||
return router.push({
|
||||
name: RouteName.RESOURCE_FOLDER,
|
||||
params: {
|
||||
path: ResourceMixin.resourcePathArray(props.resource),
|
||||
preferredUsername: props.group.preferredUsername,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMovedResourceError((e) => {
|
||||
// Snackbar.open({
|
||||
// message: e.message,
|
||||
// type: "is-danger",
|
||||
// position: "is-bottom",
|
||||
// });
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.resource-wrapper {
|
||||
@@ -147,7 +142,7 @@ export default class FolderItem extends Mixins(ResourceMixin) {
|
||||
a {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #444b5d;
|
||||
// color: #444b5d;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
@@ -171,7 +166,7 @@ a {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
color: $primary;
|
||||
// color: $primary;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
<template>
|
||||
<b-dropdown aria-role="list" position="is-bottom-left">
|
||||
<b-icon icon="dots-horizontal" slot="trigger" />
|
||||
<o-dropdown aria-role="list" position="bottom-left">
|
||||
<template #trigger>
|
||||
<DotsHorizontal />
|
||||
</template>
|
||||
|
||||
<b-dropdown-item aria-role="listitem" @click="$emit('rename')">
|
||||
<b-icon icon="pencil" />
|
||||
<o-dropdown-item
|
||||
aria-role="listitem"
|
||||
@click="$emit('rename')"
|
||||
class="inline-flex"
|
||||
>
|
||||
<Pencil />
|
||||
{{ $t("Rename") }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item aria-role="listitem" @click="$emit('move')">
|
||||
<b-icon icon="folder-move" />
|
||||
</o-dropdown-item>
|
||||
<o-dropdown-item aria-role="listitem" @click="$emit('move')">
|
||||
<FolderMove />
|
||||
{{ $t("Move") }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item aria-role="listitem" @click="$emit('delete')">
|
||||
<b-icon icon="delete" />
|
||||
</o-dropdown-item>
|
||||
<o-dropdown-item aria-role="listitem" @click="$emit('delete')">
|
||||
<Delete />
|
||||
{{ $t("Delete") }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class ResourceDropdown extends Vue {}
|
||||
<script lang="ts" setup>
|
||||
import Pencil from "vue-material-design-icons/Pencil.vue";
|
||||
import FolderMove from "vue-material-design-icons/FolderMove.vue";
|
||||
import Delete from "vue-material-design-icons/Delete.vue";
|
||||
import DotsHorizontal from "vue-material-design-icons/DotsHorizontal.vue";
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="resource-wrapper" dir="auto">
|
||||
<div class="flex flex-1 items-center w-full" dir="auto">
|
||||
<a :href="resource.resourceUrl" target="_blank">
|
||||
<div class="preview">
|
||||
<div
|
||||
@@ -8,7 +8,11 @@
|
||||
Object.keys(mapServiceTypeToIcon).includes(resource.type)
|
||||
"
|
||||
>
|
||||
<b-icon :icon="mapServiceTypeToIcon[resource.type]" size="is-large" />
|
||||
<o-icon
|
||||
:icon="mapServiceTypeToIcon[resource.type]"
|
||||
size="large"
|
||||
customSize="48"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="preview-image"
|
||||
@@ -16,91 +20,81 @@
|
||||
:style="`background-image: url(${resource.metadata.imageRemoteUrl})`"
|
||||
/>
|
||||
<div class="preview-type" v-else>
|
||||
<b-icon icon="link" size="is-large" />
|
||||
<Link :size="48" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title-wrapper">
|
||||
<div class="body flex-1 px-1 pb-1">
|
||||
<div class="flex items-center gap-1 max-w-[65vw]">
|
||||
<img
|
||||
class="favicon"
|
||||
alt=""
|
||||
v-if="resource.metadata && resource.metadata.faviconUrl"
|
||||
:src="resource.metadata.faviconUrl"
|
||||
/>
|
||||
<h3>{{ resource.title }}</h3>
|
||||
<h3 class="dark:text-white">{{ resource.title }}</h3>
|
||||
</div>
|
||||
<div class="metadata-wrapper">
|
||||
<span class="host" v-if="!inline || preview">{{ urlHostname }}</span>
|
||||
<span
|
||||
class="published-at"
|
||||
:class="{ 'is-hidden-mobile': !inline }"
|
||||
class="hidden md:inline"
|
||||
:class="{ inline }"
|
||||
v-if="resource.updatedAt || resource.publishedAt"
|
||||
>{{
|
||||
(resource.updatedAt || resource.publishedAt) |
|
||||
formatDateTimeString
|
||||
formatDateTimeString(
|
||||
resource.updatedAt ?? resource.publishedAt ?? ""
|
||||
)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<resource-dropdown
|
||||
class="actions"
|
||||
class="flex-0 block mx-auto my-2 cursor-pointer mr-2"
|
||||
v-if="!inline && !preview"
|
||||
@delete="$emit('delete', resource.id)"
|
||||
@move="$emit('move', resource)"
|
||||
@rename="$emit('rename', resource)"
|
||||
@delete="emit('delete', resource.id as string)"
|
||||
@move="emit('move', resource)"
|
||||
@rename="emit('rename', resource)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { IResource, mapServiceTypeToIcon } from "@/types/resource";
|
||||
import ResourceDropdown from "@/components/Resource/ResourceDropdown.vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import Link from "vue-material-design-icons/Link.vue";
|
||||
|
||||
@Component({
|
||||
components: { ResourceDropdown },
|
||||
})
|
||||
export default class ResourceItem extends Vue {
|
||||
@Prop({ required: true, type: Object }) resource!: IResource;
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
resource: IResource;
|
||||
inline?: boolean;
|
||||
preview?: boolean;
|
||||
}>(),
|
||||
{ inline: false, preview: false }
|
||||
);
|
||||
|
||||
@Prop({ required: false, default: false }) inline!: boolean;
|
||||
@Prop({ required: false, default: false }) preview!: boolean;
|
||||
const emit = defineEmits<{
|
||||
(e: "move", resource: IResource): void;
|
||||
(e: "rename", resource: IResource): void;
|
||||
(e: "delete", resourceID: string): void;
|
||||
}>();
|
||||
|
||||
list = [];
|
||||
const list = ref([]);
|
||||
|
||||
mapServiceTypeToIcon = mapServiceTypeToIcon;
|
||||
|
||||
get urlHostname(): string | undefined {
|
||||
if (this.resource?.resourceUrl) {
|
||||
return new URL(this.resource.resourceUrl).hostname.replace(
|
||||
/^(www\.)/,
|
||||
""
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
const urlHostname = computed((): string | undefined => {
|
||||
if (props.resource?.resourceUrl) {
|
||||
return new URL(props.resource.resourceUrl).hostname.replace(/^(www\.)/, "");
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/_mixins" as *;
|
||||
.resource-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.actions {
|
||||
flex: 0;
|
||||
display: block;
|
||||
margin: auto 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #444b5d;
|
||||
// color: #444b5d;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
@@ -125,20 +119,11 @@ a {
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 8px;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
max-width: calc(100vw - 122px);
|
||||
}
|
||||
|
||||
img.favicon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@include margin-right(6px);
|
||||
// @include margin-right(6px);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -146,7 +131,7 @@ a {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
color: $primary;
|
||||
// color: $primary;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<template>
|
||||
<div v-if="resource">
|
||||
<article class="panel is-primary">
|
||||
<p class="panel-heading truncate">
|
||||
<h2 class="panel-heading truncate">
|
||||
{{
|
||||
$t('Move "{resourceName}"', { resourceName: initialResource.title })
|
||||
}}
|
||||
</p>
|
||||
</h2>
|
||||
<a
|
||||
class="panel-block clickable"
|
||||
@click="resource = resource.parent"
|
||||
class="panel-block clickable flex gap-1 items-center"
|
||||
@click="resourcePath.path = resource?.parent?.path"
|
||||
v-if="resource.parent"
|
||||
>
|
||||
<span class="panel-icon">
|
||||
<b-icon icon="chevron-up" size="is-small" />
|
||||
<ChevronUp :size="16" />
|
||||
</span>
|
||||
{{ $t("Parent folder") }}
|
||||
</a>
|
||||
<a
|
||||
class="panel-block clickable"
|
||||
@click="resource = { path: '/', username }"
|
||||
v-else-if="resource.path.length > 1"
|
||||
class="panel-block clickable flex gap-1 items-center"
|
||||
@click="resourcePath.path = '/'"
|
||||
v-else-if="resourcePath?.path && resourcePath?.path.length > 1"
|
||||
>
|
||||
<span class="panel-icon">
|
||||
<b-icon icon="chevron-up" size="is-small" />
|
||||
<ChevronUp :size="16" />
|
||||
</span>
|
||||
{{ $t("Parent folder") }}
|
||||
</a>
|
||||
<template v-if="resource.children">
|
||||
<a
|
||||
class="panel-block flex-wrap"
|
||||
class="panel-block flex flex-wrap gap-1 px-2"
|
||||
v-for="element in resource.children.elements"
|
||||
:class="{
|
||||
clickable:
|
||||
@@ -37,14 +37,10 @@
|
||||
:key="element.id"
|
||||
@click="goDown(element)"
|
||||
>
|
||||
<p class="truncate">
|
||||
<p class="truncate flex gap-1 items-center">
|
||||
<span class="panel-icon">
|
||||
<b-icon
|
||||
icon="folder"
|
||||
size="is-small"
|
||||
v-if="element.type === 'folder'"
|
||||
/>
|
||||
<b-icon icon="link" size="is-small" v-else />
|
||||
<Folder :size="16" v-if="element.type === 'folder'" />
|
||||
<Link :size="16" v-else />
|
||||
</span>
|
||||
<span>{{ element.title }}</span>
|
||||
</p>
|
||||
@@ -60,11 +56,11 @@
|
||||
>
|
||||
{{ $t("No resources in this folder") }}
|
||||
</p>
|
||||
<b-pagination
|
||||
<o-pagination
|
||||
v-if="resource.children && resource.children.total > RESOURCES_PER_PAGE"
|
||||
:total="resource.children.total"
|
||||
v-model="page"
|
||||
size="is-small"
|
||||
size="small"
|
||||
:per-page="RESOURCES_PER_PAGE"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
@@ -72,12 +68,12 @@
|
||||
:aria-current-label="$t('Current page')"
|
||||
/>
|
||||
</article>
|
||||
<div class="buttons">
|
||||
<b-button type="is-text" @click="$emit('close-move-modal')">{{
|
||||
<div class="flex gap-2 mt-2">
|
||||
<o-button type="is-text" @click="emit('close-move-modal')">{{
|
||||
$t("Cancel")
|
||||
}}</b-button>
|
||||
<b-button
|
||||
type="is-primary"
|
||||
}}</o-button>
|
||||
<o-button
|
||||
variant="primary"
|
||||
@click="updateResource"
|
||||
:disabled="moveDisabled"
|
||||
><template v-if="resource.path === '/'">
|
||||
@@ -85,93 +81,84 @@
|
||||
</template>
|
||||
<template v-else
|
||||
>{{ $t("Move resource to {folder}", { folder: resource.title }) }}
|
||||
</template></b-button
|
||||
</template></o-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Watch } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { GET_RESOURCE } from "../../graphql/resources";
|
||||
import { IResource } from "../../types/resource";
|
||||
import Folder from "vue-material-design-icons/Folder.vue";
|
||||
import Link from "vue-material-design-icons/Link.vue";
|
||||
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
resource: {
|
||||
query: GET_RESOURCE,
|
||||
variables() {
|
||||
if (this.resource && this.resource.path) {
|
||||
return {
|
||||
path: this.resource.path,
|
||||
username: this.username,
|
||||
page: this.page,
|
||||
limit: this.RESOURCES_PER_PAGE,
|
||||
};
|
||||
}
|
||||
return { path: "/", username: this.username };
|
||||
},
|
||||
skip() {
|
||||
return !this.username;
|
||||
},
|
||||
const props = defineProps<{ initialResource: IResource; username: string }>();
|
||||
const emit = defineEmits(["update-resource", "close-move-modal"]);
|
||||
|
||||
const resourcePath = ref<{ path: string | undefined; username: string }>({
|
||||
path: props.initialResource.path,
|
||||
username: props.username,
|
||||
});
|
||||
|
||||
const RESOURCES_PER_PAGE = 10;
|
||||
const page = ref(1);
|
||||
|
||||
const { result: resourceResult, refetch } = useQuery<{ resource: IResource }>(
|
||||
GET_RESOURCE,
|
||||
() => {
|
||||
if (resourcePath.value?.path) {
|
||||
return {
|
||||
path: resourcePath.value?.path,
|
||||
username: props.username,
|
||||
page: page.value,
|
||||
limit: RESOURCES_PER_PAGE,
|
||||
};
|
||||
}
|
||||
return { path: "/", username: props.username };
|
||||
}
|
||||
);
|
||||
|
||||
const resource = computed(() => resourceResult.value?.resource);
|
||||
|
||||
const goDown = (element: IResource): void => {
|
||||
if (element.type === "folder" && element.id !== props.initialResource.id) {
|
||||
resourcePath.value.path = element.path;
|
||||
}
|
||||
};
|
||||
|
||||
watch(props.initialResource, () => {
|
||||
if (props.initialResource) {
|
||||
resourcePath.value.path = props.initialResource?.parent?.path;
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
const updateResource = (): void => {
|
||||
emit(
|
||||
"update-resource",
|
||||
{
|
||||
id: props.initialResource.id,
|
||||
title: props.initialResource.title,
|
||||
parent: resourcePath.value?.path === "/" ? null : resourcePath.value,
|
||||
path: props.initialResource.path,
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class ResourceSelector extends Vue {
|
||||
@Prop({ required: true }) initialResource!: IResource;
|
||||
props.initialResource.parent
|
||||
);
|
||||
};
|
||||
|
||||
@Prop({ required: true }) username!: string;
|
||||
|
||||
resource: IResource | undefined = undefined;
|
||||
|
||||
RESOURCES_PER_PAGE = 10;
|
||||
|
||||
page = 1;
|
||||
|
||||
goDown(element: IResource): void {
|
||||
if (element.type === "folder" && element.id !== this.initialResource.id) {
|
||||
this.resource = element;
|
||||
}
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
resource: this.initialResource?.parent,
|
||||
};
|
||||
}
|
||||
|
||||
@Watch("initialResource")
|
||||
updateResourceFromProp() {
|
||||
if (this.initialResource) {
|
||||
this.resource = this.initialResource?.parent;
|
||||
this.$apollo.queries.resource.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
updateResource(): void {
|
||||
this.$emit(
|
||||
"update-resource",
|
||||
{
|
||||
id: this.initialResource.id,
|
||||
title: this.initialResource.title,
|
||||
parent:
|
||||
this.resource && this.resource.path === "/" ? null : this.resource,
|
||||
path: this.initialResource.path,
|
||||
},
|
||||
this.initialResource.parent
|
||||
);
|
||||
}
|
||||
|
||||
get moveDisabled(): boolean | undefined {
|
||||
return (
|
||||
(this.initialResource.parent &&
|
||||
this.resource &&
|
||||
this.initialResource.parent.path === this.resource.path) ||
|
||||
(this.initialResource.parent === undefined &&
|
||||
this.resource &&
|
||||
this.resource.path === "/")
|
||||
);
|
||||
}
|
||||
}
|
||||
const moveDisabled = computed((): boolean | undefined => {
|
||||
return (
|
||||
(props.initialResource.parent &&
|
||||
resourcePath.value &&
|
||||
props.initialResource.parent.path === resourcePath.value.path) ||
|
||||
(props.initialResource.parent === undefined &&
|
||||
resourcePath.value &&
|
||||
resourcePath.value.path === "/")
|
||||
);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.panel {
|
||||
|
||||
15
js/src/components/Resource/utils.ts
Normal file
15
js/src/components/Resource/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IResource } from "@/types/resource";
|
||||
|
||||
export const resourcePath = (resource: IResource | undefined): string => {
|
||||
const path = resource?.path ?? undefined;
|
||||
if (path && path[0] === "/") {
|
||||
return path.slice(1);
|
||||
}
|
||||
return path ?? "";
|
||||
};
|
||||
|
||||
export const resourcePathArray = (
|
||||
resource: IResource | undefined
|
||||
): string[] => {
|
||||
return resourcePath(resource).split("/");
|
||||
};
|
||||
Reference in New Issue
Block a user