Merge branch 'allow-to-properly-move-group-resources' into 'master'

Allow to properly move group resources

See merge request framasoft/mobilizon!496
This commit is contained in:
Thomas Citharel 2020-06-26 15:04:10 +02:00
commit 08b8d3b491
14 changed files with 313 additions and 33 deletions

View File

@ -29,7 +29,7 @@
class="actions" class="actions"
v-if="!inline" v-if="!inline"
@delete="$emit('delete', resource.id)" @delete="$emit('delete', resource.id)"
@move="$emit('move', resource.id)" @move="$emit('move', resource)"
@rename="$emit('rename', resource)" @rename="$emit('rename', resource)"
/> />
</div> </div>
@ -70,8 +70,6 @@ export default class FolderItem extends Mixins(ResourceMixin) {
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
async onChange(evt: ChangeEvent<IResource>): Promise<Route | undefined> { async onChange(evt: ChangeEvent<IResource>): Promise<Route | undefined> {
console.log("into folder item");
console.log(evt);
if (evt.added && evt.added.element) { if (evt.added && evt.added.element) {
const movedResource = evt.added.element as IResource; const movedResource = evt.added.element as IResource;
const updatedResource = await this.moveResource(movedResource); const updatedResource = await this.moveResource(movedResource);

View File

@ -29,7 +29,7 @@
class="actions" class="actions"
v-if="!inline" v-if="!inline"
@delete="$emit('delete', resource.id)" @delete="$emit('delete', resource.id)"
@move="$emit('move', resource.id)" @move="$emit('move', resource)"
@rename="$emit('rename', resource)" @rename="$emit('rename', resource)"
/> />
</div> </div>

View File

@ -0,0 +1,129 @@
<template>
<div v-if="resource">
<article class="panel is-primary">
<p class="panel-heading">
{{ $t('Move "{resourceName}"', { resourceName: initialResource.title }) }}
</p>
<a class="panel-block clickable" @click="resource = resource.parent" v-if="resource.parent">
<span class="panel-icon">
<b-icon icon="chevron-up" size="is-small" />
</span>
{{ $t("Parent folder") }}
</a>
<a
class="panel-block clickable"
@click="resource = { path: '/', username }"
v-else-if="resource.path.length > 1"
>
<span class="panel-icon">
<b-icon icon="chevron-up" size="is-small" />
</span>
{{ $t("Parent folder") }}
</a>
<a
class="panel-block"
v-for="element in resource.children.elements"
:class="{ clickable: element.type === 'folder' && element.id !== initialResource.id }"
:key="element.id"
@click="goDown(element)"
>
<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 />
</span>
{{ element.title }}
<span v-if="element.id === initialResource.id">
<em v-if="element.type === 'folder'"> {{ $t("(this folder)") }}</em>
<em v-else> {{ $t("(this link)") }}</em>
</span>
</a>
<p
class="panel-block content has-text-grey has-text-centered"
v-if="resource.children.total === 0"
>
{{ $t("No resources in this folder") }}
</p>
</article>
<b-button type="is-primary" @click="updateResource" :disabled="moveDisabled">{{
$t("Move resource to {folder}", { folder: resource.title })
}}</b-button>
<b-button type="is-text" @click="$emit('closeMoveModal')">{{ $t("Cancel") }}</b-button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { GET_RESOURCE } from "../../graphql/resources";
import { IResource } from "../../types/resource";
@Component({
apollo: {
resource: {
query: GET_RESOURCE,
variables() {
if (this.resource && this.resource.path) {
return {
path: this.resource.path,
username: this.username,
};
}
return { path: "/", username: this.username };
},
skip() {
return !this.username;
},
},
},
})
export default class ResourceSelector extends Vue {
@Prop({ required: true }) initialResource!: IResource;
@Prop({ required: true }) username!: string;
resource: IResource | undefined = this.initialResource.parent;
goDown(element: IResource) {
if (element.type === "folder" && element.id !== this.initialResource.id) {
this.resource = element;
}
}
updateResource() {
this.$emit(
"updateResource",
{
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() {
return (
(this.initialResource.parent &&
this.resource &&
this.initialResource.parent.path === this.resource.path) ||
(this.initialResource.parent == undefined && this.resource && this.resource.path === "/")
);
}
}
</script>
<style lang="scss" scoped>
@import "../../variables.scss";
.panel {
a.panel-block {
cursor: default;
&.clickable {
cursor: pointer;
}
}
&.is-primary .panel-heading {
background: $primary;
color: #fff;
}
}
</style>

View File

@ -18,6 +18,7 @@ export const GET_RESOURCE = gql`
summary summary
url url
path path
type
metadata { metadata {
...ResourceMetadataBasicFields ...ResourceMetadataBasicFields
authorName authorName
@ -28,6 +29,8 @@ export const GET_RESOURCE = gql`
} }
parent { parent {
id id
path
type
} }
actor { actor {
id id
@ -44,6 +47,11 @@ export const GET_RESOURCE = gql`
type type
path path
resourceUrl resourceUrl
parent {
id
path
type
}
metadata { metadata {
...ResourceMetadataBasicFields ...ResourceMetadataBasicFields
} }
@ -112,7 +120,12 @@ export const UPDATE_RESOURCE = gql`
summary summary
url url
path path
type
resourceUrl resourceUrl
parent {
id
path
}
} }
} }
`; `;

View File

@ -510,10 +510,7 @@
"Website": "Website", "Website": "Website",
"Actor": "Actor", "Actor": "Actor",
"Statut": "Statut", "Statut": "Statut",
"Conversations": "Conversations",
"Text": "Text", "Text": "Text",
"New conversation": "New conversation",
"Create a new conversation": "Create a new conversation",
"All group members and other eventual server admins will still be able to view this information.": "All group members and other eventual server admins will still be able to view this information.", "All group members and other eventual server admins will still be able to view this information.": "All group members and other eventual server admins will still be able to view this information.",
"Upcoming events": "Upcoming events", "Upcoming events": "Upcoming events",
"View all upcoming events": "View all upcoming events", "View all upcoming events": "View all upcoming events",
@ -525,7 +522,6 @@
"Post a public message": "Post a public message", "Post a public message": "Post a public message",
"View all todos": "View all todos", "View all todos": "View all todos",
"Discussions": "Discussions", "Discussions": "Discussions",
"View all conversations": "View all conversations",
"No public upcoming events": "No public upcoming events", "No public upcoming events": "No public upcoming events",
"Latest posts": "Latest posts", "Latest posts": "Latest posts",
"Invite a new member": "Invite a new member", "Invite a new member": "Invite a new member",
@ -696,5 +692,16 @@
"Can be an email or a link, or just plain text.": "Can be an email or a link, or just plain text.", "Can be an email or a link, or just plain text.": "Can be an email or a link, or just plain text.",
"No profiles found": "No profiles found", "No profiles found": "No profiles found",
"URL copied to clipboard": "URL copied to clipboard", "URL copied to clipboard": "URL copied to clipboard",
"Report #{reportNumber}": "Report #{reportNumber}" "Report #{reportNumber}": "Report #{reportNumber}",
"Move \"{resourceName}\"": "Move \"{resourceName}\"",
"Parent folder": "Parent folder",
"(this folder)": "(this folder)",
"(this link)": "(this link)",
"Move resource to {folder}": "Move resource to {folder}",
"Create a folder": "Create a folder",
"No resources in this folder": "No resources in this folder",
"New discussion": "New discussion",
"Create a discussion": "Create a discussion",
"Create the discussion": "Create the discussion",
"View all discussions": "View all discussions"
} }

View File

@ -78,11 +78,9 @@
"Confirmed: Will happen": "Confirmé : aura lieu", "Confirmed: Will happen": "Confirmé : aura lieu",
"Contact": "Contact", "Contact": "Contact",
"Continue editing": "Continuer la modification", "Continue editing": "Continuer la modification",
"Conversations": "Conversations",
"Country": "Pays", "Country": "Pays",
"Create": "Créer", "Create": "Créer",
"Create a calc": "Créer un calc", "Create a calc": "Créer un calc",
"Create a new conversation": "Créer une nouvelle conversation",
"Create a new event": "Créer un nouvel évènement", "Create a new event": "Créer un nouvel évènement",
"Create a new group": "Créer un nouveau groupe", "Create a new group": "Créer un nouveau groupe",
"Create a new identity": "Créer une nouvelle identité", "Create a new identity": "Créer une nouvelle identité",
@ -260,7 +258,6 @@
"My groups": "Mes groupes", "My groups": "Mes groupes",
"My identities": "Mes identités", "My identities": "Mes identités",
"Name": "Nom", "Name": "Nom",
"New conversation": "Nouvelle conversation",
"New email": "Nouvelle adresse e-mail", "New email": "Nouvelle adresse e-mail",
"New folder": "Nouveau dossier", "New folder": "Nouveau dossier",
"New link": "Nouveau lien", "New link": "Nouveau lien",
@ -478,7 +475,6 @@
"Username": "Pseudo", "Username": "Pseudo",
"Users": "Utilisateur⋅ice⋅s", "Users": "Utilisateur⋅ice⋅s",
"View a reply": "Aucune réponse | Voir une réponse | Voir {totalReplies} réponses", "View a reply": "Aucune réponse | Voir une réponse | Voir {totalReplies} réponses",
"View all conversations": "Voir toutes les conversations",
"View all resources": "Voir toutes les resources", "View all resources": "Voir toutes les resources",
"View all todos": "Voir tous les todos", "View all todos": "Voir tous les todos",
"View all upcoming events": "Voir tous les événements à venir", "View all upcoming events": "Voir tous les événements à venir",
@ -696,5 +692,16 @@
"contact uninformed": "contact non renseigné", "contact uninformed": "contact non renseigné",
"Can be an email or a link, or just plain text.": "Peut être une adresse email ou bien un lien, ou alors du simple texte brut.", "Can be an email or a link, or just plain text.": "Peut être une adresse email ou bien un lien, ou alors du simple texte brut.",
"URL copied to clipboard": "URL copiée dans le presse-papiers", "URL copied to clipboard": "URL copiée dans le presse-papiers",
"Report #{reportNumber}": "Signalement #{reportNumber}" "Report #{reportNumber}": "Signalement #{reportNumber}",
"Move \"{resourceName}\"": "Déplacer « {resourceName} »",
"Parent folder": "Dossier parent",
"(this folder)": "(ce dossier)",
"(this link)": "(ce lien)",
"Move resource to {folder}": "Déplacer la ressource dans {folder}",
"Create a folder": "Créer un dossier",
"No resources in this folder": "Aucune ressource dans ce dossier",
"New discussion": "Nouvelle discussion",
"Create a discussion": "Créer une discussion",
"Create the discussion": "Créer la discussion",
"View all discussions": "Voir toutes les discussions"
} }

View File

@ -20,7 +20,7 @@
name: RouteName.CONVERSATION_LIST, name: RouteName.CONVERSATION_LIST,
params: { preferredUsername: conversation.actor.preferredUsername }, params: { preferredUsername: conversation.actor.preferredUsername },
}" }"
>{{ $t("Conversations") }}</router-link >{{ $t("Discussions") }}</router-link
> >
</li> </li>
<li class="is-active"> <li class="is-active">

View File

@ -20,7 +20,7 @@
name: RouteName.CONVERSATION_LIST, name: RouteName.CONVERSATION_LIST,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
}" }"
>{{ $t("Conversations") }}</router-link >{{ $t("Discussions") }}</router-link
> >
</li> </li>
</ul> </ul>
@ -39,7 +39,7 @@
name: RouteName.CREATE_CONVERSATION, name: RouteName.CREATE_CONVERSATION,
params: { preferredUsername: this.preferredUsername }, params: { preferredUsername: this.preferredUsername },
}" }"
>{{ $t("New conversation") }}</b-button >{{ $t("New discussion") }}</b-button
> >
</section> </section>
</div> </div>

View File

@ -1,6 +1,6 @@
<template> <template>
<section class="section container"> <section class="section container">
<h1>{{ $t("Create a new conversation") }}</h1> <h1>{{ $t("Create a discussion") }}</h1>
<form @submit.prevent="createConversation"> <form @submit.prevent="createConversation">
<b-field :label="$t('Title')"> <b-field :label="$t('Title')">
@ -11,7 +11,7 @@
<editor v-model="conversation.text" /> <editor v-model="conversation.text" />
</b-field> </b-field>
<button class="button is-primary" type="submit">{{ $t("Create my group") }}</button> <button class="button is-primary" type="submit">{{ $t("Create the discussion") }}</button>
</form> </form>
</section> </section>
</template> </template>

View File

@ -133,7 +133,7 @@
name: RouteName.CONVERSATION_LIST, name: RouteName.CONVERSATION_LIST,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
}" }"
>{{ $t("View all conversations") }}</router-link >{{ $t("View all discussions") }}</router-link
> >
</section> </section>
</div> </div>

View File

@ -66,7 +66,7 @@
<section> <section>
<div class="list-header"> <div class="list-header">
<div class="list-header-right"> <div class="list-header-right">
<b-checkbox v-model="checkedAll" /> <b-checkbox v-model="checkedAll" v-if="resource.children.total > 0" />
<div class="actions" v-if="validCheckedResources.length > 0"> <div class="actions" v-if="validCheckedResources.length > 0">
<small> <small>
{{ {{
@ -85,7 +85,12 @@
</div> </div>
</div> </div>
</div> </div>
<draggable v-model="resource.children.elements" :sort="false" :group="groupObject"> <draggable
v-model="resource.children.elements"
:sort="false"
:group="groupObject"
v-if="resource.children.total > 0"
>
<transition-group> <transition-group>
<div v-for="localResource in resource.children.elements" :key="localResource.id"> <div v-for="localResource in resource.children.elements" :key="localResource.id">
<div class="resource-item"> <div class="resource-item">
@ -100,18 +105,22 @@
v-if="localResource.type !== 'folder'" v-if="localResource.type !== 'folder'"
@delete="deleteResource" @delete="deleteResource"
@rename="handleRename" @rename="handleRename"
@move="handleMove"
/> />
<folder-item <folder-item
:resource="localResource" :resource="localResource"
:group="resource.actor" :group="resource.actor"
@delete="deleteResource" @delete="deleteResource"
@rename="handleRename" @move="handleMove"
v-else v-else
/> />
</div> </div>
</div> </div>
</transition-group> </transition-group>
</draggable> </draggable>
<div class="content has-text-centered has-text-grey">
<p>{{ $t("No resources in this folder") }}</p>
</div>
</section> </section>
<b-modal :active.sync="renameModal" has-modal-card> <b-modal :active.sync="renameModal" has-modal-card>
<div class="modal-card"> <div class="modal-card">
@ -126,6 +135,18 @@
</section> </section>
</div> </div>
</b-modal> </b-modal>
<b-modal :active.sync="moveModal" has-modal-card>
<div class="modal-card">
<section class="modal-card-body">
<resource-selector
:initialResource="updatedResource"
:username="resource.actor.preferredUsername"
@updateResource="moveResource"
@closeMoveModal="moveModal = false"
/>
</section>
</div>
</b-modal>
<b-modal :active.sync="createResourceModal" has-modal-card> <b-modal :active.sync="createResourceModal" has-modal-card>
<div class="modal-card"> <div class="modal-card">
<section class="modal-card-body"> <section class="modal-card-body">
@ -190,9 +211,10 @@ import {
import { CONFIG } from "../../graphql/config"; import { CONFIG } from "../../graphql/config";
import { IConfig } from "../../types/config.model"; import { IConfig } from "../../types/config.model";
import ResourceMixin from "../../mixins/resource"; import ResourceMixin from "../../mixins/resource";
import ResourceSelector from "../../components/Resource/ResourceSelector.vue";
@Component({ @Component({
components: { FolderItem, ResourceItem, Draggable }, components: { FolderItem, ResourceItem, Draggable, ResourceSelector },
apollo: { apollo: {
resource: { resource: {
query: GET_RESOURCE, query: GET_RESOURCE,
@ -253,6 +275,8 @@ export default class Resources extends Mixins(ResourceMixin) {
createLinkResourceModal = false; createLinkResourceModal = false;
moveModal = false;
renameModal = false; renameModal = false;
groupObject: object = { groupObject: object = {
@ -332,6 +356,8 @@ export default class Resources extends Mixins(ResourceMixin) {
createSentenceForType(type: string) { createSentenceForType(type: string) {
switch (type) { switch (type) {
case "folder":
return this.$t("Create a folder");
case "pad": case "pad":
return this.$t("Create a pad"); return this.$t("Create a pad");
case "calc": case "calc":
@ -347,7 +373,6 @@ export default class Resources extends Mixins(ResourceMixin) {
} }
createResourceFromProvider(provider: IProvider) { createResourceFromProvider(provider: IProvider) {
console.log(provider);
this.newResource.resourceUrl = this.generateFullResourceUrl(provider); this.newResource.resourceUrl = this.generateFullResourceUrl(provider);
this.newResource.type = provider.software; this.newResource.type = provider.software;
this.createResourceModal = true; this.createResourceModal = true;
@ -445,24 +470,100 @@ export default class Resources extends Mixins(ResourceMixin) {
handleRename(resource: IResource) { handleRename(resource: IResource) {
this.renameModal = true; this.renameModal = true;
this.updatedResource = resource; this.updatedResource = Object.assign({}, resource);
}
handleMove(resource: IResource) {
this.moveModal = true;
this.updatedResource = Object.assign({}, resource);
}
async moveResource(resource: IResource, oldParent: IResource | undefined) {
const parentPath = oldParent && oldParent.path ? oldParent.path || "/" : "/";
await this.updateResource(resource, parentPath);
this.moveModal = false;
} }
async renameResource() { async renameResource() {
await this.updateResource(this.updatedResource); await this.updateResource(this.updatedResource);
this.renameModal = false;
} }
async updateResource(resource: IResource) { async updateResource(resource: IResource, parentPath: string | null = null) {
try { try {
if (!resource.parent) return;
await this.$apollo.mutate<{ updateResource: IResource }>({ await this.$apollo.mutate<{ updateResource: IResource }>({
mutation: UPDATE_RESOURCE, mutation: UPDATE_RESOURCE,
variables: { variables: {
id: resource.id, id: resource.id,
title: resource.title, title: resource.title,
parentId: resource.parent.id, parentId: resource.parent ? resource.parent.id : null,
path: resource.path, path: resource.path,
}, },
update: (store, { data }) => {
if (!data || data.updateResource == null || parentPath == null) return;
if (!this.resource.actor) return;
console.log("Removing ressource from old parent");
const oldParentCachedData = store.readQuery<{ resource: IResource }>({
query: GET_RESOURCE,
variables: {
path: parentPath,
username: this.resource.actor.preferredUsername,
},
});
if (oldParentCachedData == null) return;
const { resource: oldParentCachedResource } = oldParentCachedData;
if (oldParentCachedResource == null) {
console.error("Cannot update resource cache, because of null value.");
return;
}
const resource: IResource = data.updateResource;
oldParentCachedResource.children.elements = oldParentCachedResource.children.elements.filter(
(cachedResource) => cachedResource.id !== resource.id
);
store.writeQuery({
query: GET_RESOURCE,
variables: {
path: parentPath,
username: this.resource.actor.preferredUsername,
},
data: { oldParentCachedResource },
});
console.log("Finished removing ressource from old parent");
console.log("Adding resource to new parent");
if (!resource.parent || !resource.parent.path) {
console.log("No cache found for new parent");
return;
}
const newParentCachedData = store.readQuery<{ resource: IResource }>({
query: GET_RESOURCE,
variables: {
path: resource.parent.path,
username: this.resource.actor.preferredUsername,
},
});
if (newParentCachedData == null) return;
const { resource: newParentCachedResource } = newParentCachedData;
if (newParentCachedResource == null) {
console.error("Cannot update resource cache, because of null value.");
return;
}
newParentCachedResource.children.elements.push(resource);
store.writeQuery({
query: GET_RESOURCE,
variables: {
path: resource.parent.path,
username: this.resource.actor.preferredUsername,
},
data: { newParentCachedResource },
});
console.log("Finished adding resource to new parent");
},
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@ -5,7 +5,18 @@ defmodule Mobilizon.GraphQL.Schema do
use Absinthe.Schema use Absinthe.Schema
alias Mobilizon.{Actors, Addresses, Conversations, Events, Media, Reports, Todos, Users} alias Mobilizon.{
Actors,
Addresses,
Conversations,
Events,
Media,
Reports,
Resources,
Todos,
Users
}
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Conversations.Comment alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
@ -109,6 +120,7 @@ defmodule Mobilizon.GraphQL.Schema do
|> Dataloader.add_source(Addresses, default_source) |> Dataloader.add_source(Addresses, default_source)
|> Dataloader.add_source(Media, default_source) |> Dataloader.add_source(Media, default_source)
|> Dataloader.add_source(Reports, default_source) |> Dataloader.add_source(Reports, default_source)
|> Dataloader.add_source(Resources, default_source)
|> Dataloader.add_source(Todos, default_source) |> Dataloader.add_source(Todos, default_source)
Map.put(ctx, :loader, loader) Map.put(ctx, :loader, loader)

View File

@ -4,6 +4,8 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
""" """
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
alias Mobilizon.GraphQL.Resolvers.Resource alias Mobilizon.GraphQL.Resolvers.Resource
alias Mobilizon.Resources
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
@desc "A resource" @desc "A resource"
object :resource do object :resource do
@ -19,7 +21,8 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
field(:updated_at, :naive_datetime, description: "The resource's last update date") field(:updated_at, :naive_datetime, description: "The resource's last update date")
field(:type, :string, description: "The resource's type (if it's a folder)") field(:type, :string, description: "The resource's type (if it's a folder)")
field(:path, :string, description: "The resource's path") field(:path, :string, description: "The resource's path")
field(:parent, :resource, description: "The resource's parent")
field(:parent, :resource, description: "The resource's parent", resolve: dataloader(Resources))
field :children, :paginated_resource_list do field :children, :paginated_resource_list do
description("Children resources in folder") description("Children resources in folder")

View File

@ -30,7 +30,17 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OGP do
defp transform_tags(data) do defp transform_tags(data) do
data data
|> Map.put(:image_remote_url, Map.get(data, :image)) |> Map.put(:image_remote_url, Map.get(data, :image))
|> Map.put(:width, Map.get(data, :"image:width")) |> Map.put(:width, get_integer_value(data, :"image:width"))
|> Map.put(:height, Map.get(data, :"image:height")) |> Map.put(:height, get_integer_value(data, :"image:height"))
end
@spec get_integer_value(Map.t(), atom()) :: integer() | nil
defp get_integer_value(data, key) do
with value when not is_nil(value) <- Map.get(data, key),
{value, ""} <- Integer.parse(value) do
value
else
_ -> nil
end
end end
end end