diff --git a/addon/components/category-nav/category.hbs b/addon/components/category-nav/category.hbs
index 5206237f..abfbc83d 100644
--- a/addon/components/category-nav/category.hbs
+++ b/addon/components/category-nav/category.hbs
@@ -1,4 +1,11 @@
-
0;
+ }
+
+ @action onDragOver(event) {
+ if (!this.args.category.id) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ @dropTask
+ *onDrop(event) {
+ event.preventDefault();
+
+ if (!this.args.category.id) return;
+
+ const documentIds = event.dataTransfer.getData("text").split(",");
+
+ const success = yield Promise.all(
+ documentIds.map(async (id) => {
+ const document = this.store.peekRecord("document", id);
+
+ if (document.category.id === this.args.category.id) {
+ return true;
+ }
+
+ const previousCategory = this.store.peekRecord(
+ "category",
+ document.category.id,
+ );
+
+ try {
+ document.category = this.args.category;
+ await document.save();
+ return true;
+ } catch (error) {
+ document.category = previousCategory;
+
+ if (error.errors[0].status === "403") {
+ this.notification.danger(
+ this.intl.t("alexandria.errors.no-permission"),
+ );
+ }
+
+ return false;
+ }
+ }),
+ );
+
+ const failCount = success.filter((i) => i === false).length;
+ const successCount = success.filter((i) => i === true).length;
+
+ if (failCount) {
+ this.notification.danger(
+ this.intl.t("alexandria.errors.move-document", {
+ count: failCount,
+ }),
+ );
+ }
+
+ if (successCount) {
+ this.notification.success(
+ this.intl.t("alexandria.success.move-document", {
+ count: successCount,
+ }),
+ );
+
+ this.router.transitionTo(this.router.currentRouteName, {
+ queryParams: {
+ category: this.args.category.id,
+ search: undefined,
+ document: documentIds.join(","),
+ tags: [],
+ marks: [],
+ },
+ });
+ }
+ }
}
diff --git a/addon/components/document-grid.hbs b/addon/components/document-grid.hbs
index 4f94ae11..d9c5949e 100644
--- a/addon/components/document-grid.hbs
+++ b/addon/components/document-grid.hbs
@@ -18,8 +18,10 @@
data-test-document
@document={{document}}
@isSelected={{includes document.id (map-by "id" @selectedDocuments)}}
+ draggable="true"
{{on "click" (fn @onClickDocument document)}}
{{on "dblclick" (fn @onDoubleClickDocument document)}}
+ {{on "dragstart" (fn @onDragStart document)}}
/>
{{else}}
diff --git a/addon/components/document-list-item.hbs b/addon/components/document-list-item.hbs
index 08b8d570..cca478bc 100644
--- a/addon/components/document-list-item.hbs
+++ b/addon/components/document-list-item.hbs
@@ -3,8 +3,10 @@
data-test-document-list-item
data-test-document-list-item-id={{@document.id}}
tabindex="0"
+ draggable="true"
{{on "click" (fn @onClickDocument @document)}}
{{on "dblclick" (fn @onDoubleClickDocument @document)}}
+ {{on "dragstart" (fn @onDragStart @document)}}
>
{{/each}}
diff --git a/addon/components/document-view.hbs b/addon/components/document-view.hbs
index 7c7d0c02..077cde6a 100644
--- a/addon/components/document-view.hbs
+++ b/addon/components/document-view.hbs
@@ -56,6 +56,7 @@
@selectedDocuments={{this.documents.selectedDocuments}}
@onClickDocument={{this.handleDocumentSelection}}
@onDoubleClickDocument={{this.openDocument}}
+ @onDragStart={{this.dragDocument}}
/>
{{else}}
{{/if}}
@@ -85,4 +87,11 @@
+
+
+
+ {{t "alexandria.move-document" count=this.documents.selectedDocuments.length}}
\ No newline at end of file
diff --git a/addon/components/document-view.js b/addon/components/document-view.js
index af971bf8..8e46cb24 100644
--- a/addon/components/document-view.js
+++ b/addon/components/document-view.js
@@ -23,6 +23,8 @@ export default class DocumentViewComponent extends Component {
// Needed for ember-resource
@tracked uploadedDocuments = 0;
+ dragElement = null;
+
constructor(parent, args) {
super(parent, args);
/* Adds a key down event listener to enable Ctrl+A document selection of all docs
@@ -105,7 +107,9 @@ export default class DocumentViewComponent extends Component {
}
onDrop = dropTask(async (event) => {
- if (!this.args.filters.category) {
+ if (!this.args.filters.category || !event.dataTransfer.files.length) {
+ this.dragCounter = 0;
+ this.isDragOver = false;
return;
}
@@ -132,6 +136,7 @@ export default class DocumentViewComponent extends Component {
);
}
+ this.dragCounter = 0;
this.isDragOver = false;
});
@@ -208,4 +213,21 @@ export default class DocumentViewComponent extends Component {
afterUpload() {
this.uploadedDocuments++;
}
+
+ @action registerDragInfo(element) {
+ this.dragInfo = element;
+ }
+
+ @action dragDocument(document, event) {
+ if (!this.documents.selectedDocuments.includes(document)) {
+ this.handleDocumentSelection(document, event);
+ }
+
+ event.dataTransfer.clearData();
+ event.dataTransfer.setData(
+ "text/plain",
+ this.documents.selectedDocuments.map((d) => d.id).join(","),
+ );
+ event.dataTransfer.setDragImage(this.dragInfo, -20, 0);
+ }
}
diff --git a/app/styles/components/_drop.scss b/app/styles/components/_drop.scss
index 81d34b12..33b6e949 100644
--- a/app/styles/components/_drop.scss
+++ b/app/styles/components/_drop.scss
@@ -1,3 +1,5 @@
+@use "sass:math";
+
.drop {
width: 200px;
@@ -21,3 +23,12 @@
}
}
}
+
+.drag-info {
+ padding: math.div($padding-small-padding, 3) $padding-small-padding;
+ border-color: $global-primary-background;
+ position: fixed;
+ top: 0;
+ right: 0;
+ transform: translate(100%, -100%);
+}
diff --git a/app/styles/components/category-nav/_category.scss b/app/styles/components/category-nav/_category.scss
index 97ec3ca1..28b19e44 100644
--- a/app/styles/components/category-nav/_category.scss
+++ b/app/styles/components/category-nav/_category.scss
@@ -2,23 +2,34 @@
background: $background-default-background;
}
-.uk-nav .category-nav__category > div {
- transition-property: background, box-shadow, color;
- transition-duration: $animation-duration;
- padding: 10px 0 10px $global-margin;
- cursor: pointer;
+.uk-nav .category-nav__category {
+ &--dragover {
+ outline: 1px solid $alert-primary-color;
+ outline-offset: -1px;
- &.active {
- @include active;
+ > div {
+ background-color: rgba($alert-primary-color, 0.25) !important;
+ }
}
- &:hover {
- @include active;
- @extend .uk-box-shadow-small;
- }
+ > div {
+ transition-property: background, box-shadow, color;
+ transition-duration: $animation-duration;
+ padding: 10px 0 10px $global-margin;
+ cursor: pointer;
+
+ &.active {
+ @include active;
+ }
+
+ &:hover {
+ @include active;
+ @extend .uk-box-shadow-small;
+ }
- .skeleton-text {
- height: 15px;
- width: 70%;
+ .skeleton-text {
+ height: 15px;
+ width: 70%;
+ }
}
}
diff --git a/tests/dummy/mirage/config.js b/tests/dummy/mirage/config.js
index 1ae13f35..5fa0dd27 100644
--- a/tests/dummy/mirage/config.js
+++ b/tests/dummy/mirage/config.js
@@ -9,21 +9,10 @@ export default function makeServer(config) {
this.namespace = "/api/v1";
this.timing = 400;
- this.get("/categories");
- this.get("/categories/:id");
-
- this.get("/documents");
- this.get("/documents/:id");
- this.patch("/documents/:id");
- this.post("/documents");
- this.delete("/documents/:id");
-
- this.get("/tags");
- this.get("/tags/:id");
- this.post("/tags");
- this.patch("/tags/:id");
-
- this.get("/marks");
+ this.resource("categories", { only: ["index", "show"] });
+ this.resource("documents");
+ this.resource("tags", { except: ["delete"] });
+ this.resource("marks", { only: ["index"] });
this.post("/files", function (schema) {
const attrs = this.normalizedRequestAttrs();
diff --git a/tests/dummy/mirage/serializers/application.js b/tests/dummy/mirage/serializers/application.js
index c2f5e488..7d0057b5 100644
--- a/tests/dummy/mirage/serializers/application.js
+++ b/tests/dummy/mirage/serializers/application.js
@@ -1,3 +1,5 @@
import { JSONAPISerializer } from "miragejs";
-export default JSONAPISerializer.extend({});
+export default JSONAPISerializer.extend({
+ alwaysIncludeLinkageData: true,
+});
diff --git a/tests/integration/components/category-nav/category-test.js b/tests/integration/components/category-nav/category-test.js
index f0d04a36..aed17301 100644
--- a/tests/integration/components/category-nav/category-test.js
+++ b/tests/integration/components/category-nav/category-test.js
@@ -1,10 +1,13 @@
-import { render } from "@ember/test-helpers";
+import { render, triggerEvent } from "@ember/test-helpers";
import { setupRenderingTest } from "dummy/tests/helpers";
import { hbs } from "ember-cli-htmlbars";
+import { setupMirage } from "ember-cli-mirage/test-support";
import { module, test } from "qunit";
+import { fake } from "sinon";
module("Integration | Component | category-nav/category", function (hooks) {
setupRenderingTest(hooks);
+ setupMirage(hooks);
test("it renders a category", async function (assert) {
this.category = { name: "test", color: "#f00", id: 1 };
@@ -24,4 +27,42 @@ module("Integration | Component | category-nav/category", function (hooks) {
assert.dom("[data-test-icon]").hasStyle({ color: "rgb(255, 0, 0)" });
assert.dom("[data-test-link]").hasClass("active");
});
+
+ test("it moves dropped documents to new category", async function (assert) {
+ const category = this.server.create("category");
+ const documents = this.server.createList("document", 2, {
+ categoryId: this.server.create("category").id,
+ });
+
+ const store = this.owner.lookup("service:store");
+
+ this.category = await store.findRecord("category", category.id);
+ await store.findAll("document"); // the code uses peekRecord
+
+ const fakeTransition = fake();
+ this.owner.lookup("service:router").transitionTo = fakeTransition;
+
+ await render(
+ hbs``,
+ );
+
+ await triggerEvent("[data-test-drop]", "drop", {
+ dataTransfer: { getData: () => documents.map((d) => d.id).join(",") },
+ });
+
+ assert.strictEqual(fakeTransition.callCount, 1);
+ assert.deepEqual(fakeTransition.args[0][1], {
+ queryParams: {
+ category: this.category.id,
+ document: documents.map((d) => d.id).join(),
+ marks: [],
+ search: undefined,
+ tags: [],
+ },
+ });
+ assert.deepEqual(
+ documents.map((d) => d.category.id),
+ [this.category.id, this.category.id],
+ );
+ });
});
diff --git a/tests/integration/components/document-list-item-test.js b/tests/integration/components/document-list-item-test.js
index b6397589..9f83da7b 100644
--- a/tests/integration/components/document-list-item-test.js
+++ b/tests/integration/components/document-list-item-test.js
@@ -1,8 +1,9 @@
-import { click, doubleClick, render } from "@ember/test-helpers";
+import { click, doubleClick, render, triggerEvent } from "@ember/test-helpers";
import { setupRenderingTest } from "dummy/tests/helpers";
import { hbs } from "ember-cli-htmlbars";
import { setupMirage } from "ember-cli-mirage/test-support";
import { module, test } from "qunit";
+import { fake } from "sinon";
module("Integration | Component | document-list-item", function (hooks) {
setupRenderingTest(hooks);
@@ -16,8 +17,9 @@ module("Integration | Component | document-list-item", function (hooks) {
marks: [],
};
this.isSelected = false;
- this.onClickDocument = () => {};
- this.onDoubleClickDocument = () => {};
+ this.onClickDocument = fake();
+ this.onDoubleClickDocument = fake();
+ this.onDragStart = fake();
await render(hbs`
`);
});
@@ -37,18 +40,23 @@ module("Integration | Component | document-list-item", function (hooks) {
});
test("it fires the onClickDocument function with the correct parameter", async function (assert) {
- this.set("onClickDocument", (arg) => {
- assert.strictEqual(arg, this.document);
- });
-
await click("[data-test-document-list-item]");
+
+ assert.strictEqual(this.onClickDocument.callCount, 1);
+ assert.deepEqual(this.onClickDocument.args[0][0], this.document);
});
test("it fires the onDoubleClickDocument function with the correct parameter", async function (assert) {
- this.set("onDoubleClickDocument", (arg) => {
- assert.strictEqual(arg, this.document);
- });
-
await doubleClick("[data-test-document-list-item]");
+
+ assert.strictEqual(this.onDoubleClickDocument.callCount, 1);
+ assert.deepEqual(this.onDoubleClickDocument.args[0][0], this.document);
+ });
+
+ test("it fires the onDragStart function with the correct parameter", async function (assert) {
+ await triggerEvent("[data-test-document-list-item]", "dragstart");
+
+ assert.strictEqual(this.onDragStart.callCount, 1);
+ assert.deepEqual(this.onDragStart.args[0][0], this.document);
});
});
diff --git a/translations/de.yaml b/translations/de.yaml
index d3f0d7bb..1eebecb9 100644
--- a/translations/de.yaml
+++ b/translations/de.yaml
@@ -5,6 +5,7 @@ alexandria:
delete: "Löschen"
upload-file: "Datei hochladen"
download: "Download"
+ move-document: "{count} {count, plural, one {Dokument} other {Dokumente}} verschieben"
errors:
save-file: "Beim Herunterladen der Datei ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut."
@@ -18,6 +19,7 @@ alexandria:
} ist ein Fehler aufgetreten.
update: "Änderungen konnten nicht gespeichert werden. Versuchen Sie es erneut."
no-permission: "Sie haben keine Berechtigung, diese Aktion auszuführen."
+ move-document: "Beim Verschieben {count, plural, one {des Dokumentes} other {von # Dokumenten}} ist ein Fehler aufgetreten"
success:
delete-document: "Das Dokument wurde erfolgreich gelöscht."
@@ -27,6 +29,7 @@ alexandria:
other {Die Dokumente wurden}
} erfolgreich hochgeladen.
update: "Änderungen wurden gespeichert."
+ move-document: "{count, plural, one {Das Dokument wurde} other {# Dokumente wurden}} erfolgreich verschoben"
category-nav:
all-files: "Alle Dokumente"
diff --git a/translations/en.yaml b/translations/en.yaml
index f4260628..66f32459 100644
--- a/translations/en.yaml
+++ b/translations/en.yaml
@@ -5,6 +5,7 @@ alexandria:
delete: "Delete"
upload-file: "Upload file"
download: "Download"
+ move-document: "Move {count} {count, plural, one {document} other {documents}}"
errors:
save-file: "While downloading the document, an error occured. Please try again."
@@ -18,6 +19,7 @@ alexandria:
}, an error occured.
update: "Your changes chould'nt be saved. Please try again."
no-permission: "You don't have permission to perform this action."
+ move-document: "While moving {count, plural, one {the document} other {# documents}}, an error occured. Please try again."
success:
delete-document: "Document deleted successfully"
@@ -27,6 +29,7 @@ alexandria:
other {documents were}
} uploaded successfully.
update: "Changes saved."
+ move-document: "{count, plural, one {Document} other {# documents}} moved successfully"
category-nav:
all-files: "All documents"
|