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 @@ -
  • +
  • {{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"