From bb5a078f35fd17272c6534d7e733256568c29baa Mon Sep 17 00:00:00 2001 From: Attila Kerekes Date: Sat, 26 Nov 2022 14:35:36 +0100 Subject: [PATCH] feat: Add export import --- app/Http/Controllers/ImportController.php | 31 +++++ app/Http/Controllers/ItemController.php | 13 +- app/Http/Controllers/ItemRestController.php | 111 +++++++++++++++ app/Item.php | 3 + database/factories/ItemFactory.php | 30 ++++ public/js/app.js | 2 +- public/mix-manifest.json | 2 +- resources/assets/js/itemExport.js | 48 +++++++ resources/assets/js/itemImport.js | 128 ++++++++++++++++++ resources/lang/en/app.php | 2 + resources/views/items/import.blade.php | 32 +++++ resources/views/items/list.blade.php | 2 + routes/web.php | 3 + tests/Feature/ExampleTest.php | 4 +- tests/Feature/ItemExportTest.php | 81 +++++++++++ .../database/seeders/SettingsSeederTest.php | 2 +- tests/Unit/lang/LangTest.php | 2 +- vendor/composer/autoload_classmap.php | 1 + vendor/composer/autoload_static.php | 1 + webpack.mix.js | 28 ++-- 20 files changed, 505 insertions(+), 21 deletions(-) create mode 100644 app/Http/Controllers/ImportController.php create mode 100644 app/Http/Controllers/ItemRestController.php create mode 100644 database/factories/ItemFactory.php create mode 100644 resources/assets/js/itemExport.js create mode 100644 resources/assets/js/itemImport.js create mode 100644 resources/views/items/import.blade.php create mode 100644 tests/Feature/ItemExportTest.php diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php new file mode 100644 index 00000000..78debb0a --- /dev/null +++ b/app/Http/Controllers/ImportController.php @@ -0,0 +1,31 @@ +middleware('allowed'); + } + + /** + * Handle the incoming request. + * + * @param Request $request + * @return View + */ + public function __invoke(Request $request): View + { + return view('items.import'); + } +} diff --git a/app/Http/Controllers/ItemController.php b/app/Http/Controllers/ItemController.php index 691bb9d9..2b62e9f2 100644 --- a/app/Http/Controllers/ItemController.php +++ b/app/Http/Controllers/ItemController.php @@ -12,9 +12,9 @@ use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\ServerException; use Illuminate\Contracts\View\View; +use Illuminate\Database\Eloquent\Model; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\Routing\Redirector; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\URL; @@ -191,10 +191,10 @@ class ItemController extends Controller /** * @param Request $request - * @param $id - * @return void + * @param null $id + * @return Item */ - public function storelogic(Request $request, $id = null) + public static function storelogic(Request $request, $id = null): Item { $application = Application::single($request->input('appid')); $validatedData = $request->validate([ @@ -275,6 +275,7 @@ class ItemController extends Controller } $item->parents()->sync($request->tags); + return $item; } /** @@ -285,7 +286,7 @@ class ItemController extends Controller */ public function store(Request $request): RedirectResponse { - $this->storelogic($request); + self::storelogic($request); $route = route('dash', []); @@ -313,7 +314,7 @@ class ItemController extends Controller */ public function update(Request $request, int $id): RedirectResponse { - $this->storelogic($request, $id); + self::storelogic($request, $id); $route = route('dash', []); return redirect($route) diff --git a/app/Http/Controllers/ItemRestController.php b/app/Http/Controllers/ItemRestController.php new file mode 100644 index 00000000..be0353a4 --- /dev/null +++ b/app/Http/Controllers/ItemRestController.php @@ -0,0 +1,111 @@ +middleware('allowed'); + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + $columns = [ + 'title', + 'colour', + 'url', + 'description', + 'appid', + 'appdescription', + ]; + + return Item::select($columns) + ->where('deleted_at', null) + ->where('type', '0') + ->orderBy('order', 'asc') + ->get(); + } + + /** + * Show the form for creating a new resource. + * + * @return Response + */ + public function create() + { + + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * @return object + */ + public function store(Request $request): object + { + $item = ItemController::storelogic($request); + + if ($item) { + return (object) ['status' => 'OK']; + } + + return (object) ['status' => 'FAILED']; + } + + /** + * Display the specified resource. + * + * @param Item $item + * @return Response + */ + public function show(Item $item) + { + // + } + + /** + * Show the form for editing the specified resource. + * + * @param Item $item + * @return Response + */ + public function edit(Item $item) + { + // + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * @param Item $item + * @return Response + */ + public function update(Request $request, Item $item) + { + // + } + + /** + * Remove the specified resource from storage. + * + * @param Item $item + * @return Response + */ + public function destroy(Item $item) + { + // + } +} diff --git a/app/Item.php b/app/Item.php index 39a83e84..82277b67 100644 --- a/app/Item.php +++ b/app/Item.php @@ -4,6 +4,7 @@ namespace App; use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -15,6 +16,8 @@ class Item extends Model { use SoftDeletes; + use HasFactory; + /** * @return void */ diff --git a/database/factories/ItemFactory.php b/database/factories/ItemFactory.php new file mode 100644 index 00000000..310d2559 --- /dev/null +++ b/database/factories/ItemFactory.php @@ -0,0 +1,30 @@ + $this->faker->unique()->text(), + 'url' => $this->faker->unique()->url(), + ]; + } +} diff --git a/public/js/app.js b/public/js/app.js index bd4ef509..d32b01b6 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1 +1 @@ -function _typeof(t){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_typeof(t)}!function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==("undefined"==typeof module?"undefined":_typeof(module))&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,(function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var n=this._events=this._events||{},i=n[t]=n[t]||[];return-1==i.indexOf(e)&&i.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var n=this._onceEvents=this._onceEvents||{};return(n[t]=n[t]||{})[e]=!0,this}},e.off=function(t,e){var n=this._events&&this._events[t];if(n&&n.length){var i=n.indexOf(e);return-1!=i&&n.splice(i,1),this}},e.emitEvent=function(t,e){var n=this._events&&this._events[t];if(n&&n.length){var i=0,o=n[i];e=e||[];for(var s=this._onceEvents&&this._onceEvents[t];o;){var r=s&&s[o];r&&(this.off(t,o),delete s[o]),o.apply(this,e),o=n[i+=r?0:1]}return this}},t})),function(t,e){"function"==typeof define&&define.amd?define("unipointer/unipointer",["ev-emitter/ev-emitter"],(function(n){return e(t,n)})):"object"==("undefined"==typeof module?"undefined":_typeof(module))&&module.exports?module.exports=e(t,require("ev-emitter")):t.Unipointer=e(t,t.EvEmitter)}(window,(function(t,e){function n(){}var i=n.prototype=Object.create(e.prototype);i.bindStartEvent=function(t){this._bindStartEvent(t,!0)},i.unbindStartEvent=function(t){this._bindStartEvent(t,!1)},i._bindStartEvent=function(e,n){var i=(n=void 0===n||!!n)?"addEventListener":"removeEventListener";t.navigator.pointerEnabled?e[i]("pointerdown",this):t.navigator.msPointerEnabled?e[i]("MSPointerDown",this):(e[i]("mousedown",this),e[i]("touchstart",this))},i.handleEvent=function(t){var e="on"+t.type;this[e]&&this[e](t)},i.getTouch=function(t){for(var e=0;e.5;var o=this.colorGrid[e.toUpperCase()];this.updateCursor(o),this.setTexts(),this.setBackgrounds(),n||this.emitEvent("change",[e,t.hue,t.sat,t.lum])}},c.setTexts=function(){if(this.setTextElems)for(var t=0;t0&&s.attr("value",o)}$(".message-container").length&&setTimeout((function(){$(".message-container").fadeOut()}),3500),void 0!==document.hidden?(t="hidden",e="visibilitychange"):void 0!==document.msHidden?(t="msHidden",e="msvisibilitychange"):void 0!==document.webkitHidden&&(t="webkitHidden",e="webkitvisibilitychange");var r=[],a=[],h=$(".livestats-container");h.length>0&&(void 0===document.addEventListener||void 0===t?console.log("This browser does not support visibilityChange"):document.addEventListener(e,(function(){document[t]?r.forEach((function(t){window.clearTimeout(t)})):a.forEach((function(t){t()}))}),!1),h.each((function(t){var e=$(this).data("id"),i=1===$(this).data("dataonly")?2e4:1e3,o=$(this),s=5e3,h=function a(){$.ajax({url:"".concat(n,"get_stats/").concat(e),dataType:"json",success:function(t){o.html(t.html),"active"===t.status?s=i:s<3e4&&(s+=2e3)},complete:function(e){e.status>299||(r[t]=window.setTimeout(a,s))}})};a[t]=h,h()}))),$("#upload").change((function(){!function(t){if(t.files&&t.files[0]){var e=new FileReader;e.onload=function(t){$("#appimage img").attr("src",t.target.result)},e.readAsDataURL(t.files[0])}}(this)})),$("#sortable").sortable({stop:function(){var t=$("#sortable").sortable("toArray",{attribute:"data-id"});$.post("".concat(n,"order"),{order:t})}}),$("#sortable").sortable("disable"),$("#main").on("mouseenter","#sortable.ui-sortable-disabled .item",(function(){$(this).siblings(".tooltip").addClass("active"),$(".refresh",this).addClass("active")})).on("mouseleave",".item",(function(){$(this).siblings(".tooltip").removeClass("active"),$(".refresh",this).removeClass("active")})),$("#config-buttons").on("mouseenter","a",(function(){$(".tooltip",this).addClass("active")})).on("mouseleave","a",(function(){$(".tooltip",this).removeClass("active")})),$(".searchform > form").on("submit",(function(t){"tiles"===$("#search-container select[name=provider]").val()&&t.preventDefault()})),$("#search-container").on("input","input[name=q]",(function(){var t=this.value,e=$("#sortable").children(".item-container");"tiles"===$("#search-container select[name=provider]").val()&&t.length>0?(e.hide(),e.filter((function(){return $(this).data("name").toLowerCase().includes(t.toLowerCase())})).show()):e.show()})).on("change","select[name=provider]",(function(){var t=$("#sortable").children(".item-container");if("tiles"===$(this).val()){$("#search-container button").hide();var e=$("#search-container input[name=q]").val();e.length>0?(t.hide(),t.filter((function(){return $(this).data("name").toLowerCase().includes(e.toLowerCase())})).show()):t.show()}else $("#search-container button").show(),t.show()})),$("#app").on("click","#config-button",(function(t){t.preventDefault();var e=$("#app"),n=e.hasClass("header");e.toggleClass("header"),n?($(".add-item").hide(),$(".item-edit").hide(),$("#app").removeClass("sidebar"),$("#sortable .tooltip").css("display",""),$("#sortable").sortable("disable")):($("#sortable .tooltip").css("display","none"),$("#sortable").sortable("enable"),setTimeout((function(){$(".add-item").fadeIn(),$(".item-edit").fadeIn()}),350))})).on("click","#add-item, #pin-item",(function(t){t.preventDefault(),$("#app").toggleClass("sidebar")})).on("click",".close-sidenav",(function(t){t.preventDefault(),$("#app").removeClass("sidebar")})).on("click","#test_config",(function(t){t.preventDefault();var e=$("#create input[name=url]").val(),i=$('#sapconfig input[name="config[override_url]"]').val();i.length&&""!==i&&(e=i);var s={};s.url=e,$(".config-item").each((function(){var t=$(this).data("config");s[t]=$(this).val()})),s.id=$("form[data-item-id]").data("item-id"),s.password&&s.password===o&&(s.password=""),$.post("".concat(n,"test_config"),{data:s},(function(t){alert(t)}))})),$("#pinlist").on("click","a",(function(t){t.preventDefault();var e=$(this),i=e.data("id"),o=e.data("tag");$.get("".concat(n,"items/pintoggle/").concat(i,"/true/").concat(o),(function(t){var n=$(t).filter("#sortable").html();$("#sortable").html(n),e.toggleClass("active")}))})),$("#itemform").on("submit",(function(){var t=$('input[name="config[password]"]').first();t.length>0&&t.attr("value")===o&&t.attr("value","")}))}));var focusSearch=function(t){var e=document.querySelector('input[name="q"]');e&&(t.preventDefault(),e.focus())},openFirstNonHiddenItem=function(t){if(t.target===document.querySelector('input[name="q"]')&&"tiles"===document.querySelector("#search-container select[name=provider]").value){var e=document.querySelector('#sortable section.item-container:not([style="display: none;"]) a');"href"in e&&(t.preventDefault(),window.open(e.href))}},KEY_BINDINGS={"/":focusSearch,Enter:openFirstNonHiddenItem};document.addEventListener("keydown",(function(t){try{t.key in KEY_BINDINGS&&KEY_BINDINGS[t.key](t)}catch(t){}})); +function _typeof(t){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_typeof(t)}!function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==("undefined"==typeof module?"undefined":_typeof(module))&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,(function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var n=this._events=this._events||{},i=n[t]=n[t]||[];return-1==i.indexOf(e)&&i.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var n=this._onceEvents=this._onceEvents||{};return(n[t]=n[t]||{})[e]=!0,this}},e.off=function(t,e){var n=this._events&&this._events[t];if(n&&n.length){var i=n.indexOf(e);return-1!=i&&n.splice(i,1),this}},e.emitEvent=function(t,e){var n=this._events&&this._events[t];if(n&&n.length){var i=0,o=n[i];e=e||[];for(var s=this._onceEvents&&this._onceEvents[t];o;){var r=s&&s[o];r&&(this.off(t,o),delete s[o]),o.apply(this,e),o=n[i+=r?0:1]}return this}},t})),function(t,e){"function"==typeof define&&define.amd?define("unipointer/unipointer",["ev-emitter/ev-emitter"],(function(n){return e(t,n)})):"object"==("undefined"==typeof module?"undefined":_typeof(module))&&module.exports?module.exports=e(t,require("ev-emitter")):t.Unipointer=e(t,t.EvEmitter)}(window,(function(t,e){function n(){}var i=n.prototype=Object.create(e.prototype);i.bindStartEvent=function(t){this._bindStartEvent(t,!0)},i.unbindStartEvent=function(t){this._bindStartEvent(t,!1)},i._bindStartEvent=function(e,n){var i=(n=void 0===n||!!n)?"addEventListener":"removeEventListener";t.navigator.pointerEnabled?e[i]("pointerdown",this):t.navigator.msPointerEnabled?e[i]("MSPointerDown",this):(e[i]("mousedown",this),e[i]("touchstart",this))},i.handleEvent=function(t){var e="on"+t.type;this[e]&&this[e](t)},i.getTouch=function(t){for(var e=0;e.5;var o=this.colorGrid[e.toUpperCase()];this.updateCursor(o),this.setTexts(),this.setBackgrounds(),n||this.emitEvent("change",[e,t.hue,t.sat,t.lum])}},h.setTexts=function(){if(this.setTextElems)for(var t=0;t0&&s.attr("value",o)}$(".message-container").length&&setTimeout((function(){$(".message-container").fadeOut()}),3500),void 0!==document.hidden?(t="hidden",e="visibilitychange"):void 0!==document.msHidden?(t="msHidden",e="msvisibilitychange"):void 0!==document.webkitHidden&&(t="webkitHidden",e="webkitvisibilitychange");var r=[],a=[],c=$(".livestats-container");c.length>0&&(void 0===document.addEventListener||void 0===t?console.log("This browser does not support visibilityChange"):document.addEventListener(e,(function(){document[t]?r.forEach((function(t){window.clearTimeout(t)})):a.forEach((function(t){t()}))}),!1),c.each((function(t){var e=$(this).data("id"),i=1===$(this).data("dataonly")?2e4:1e3,o=$(this),s=5e3,c=function a(){$.ajax({url:"".concat(n,"get_stats/").concat(e),dataType:"json",success:function(t){o.html(t.html),"active"===t.status?s=i:s<3e4&&(s+=2e3)},complete:function(e){e.status>299||(r[t]=window.setTimeout(a,s))}})};a[t]=c,c()}))),$("#upload").change((function(){!function(t){if(t.files&&t.files[0]){var e=new FileReader;e.onload=function(t){$("#appimage img").attr("src",t.target.result)},e.readAsDataURL(t.files[0])}}(this)})),$("#sortable").sortable({stop:function(){var t=$("#sortable").sortable("toArray",{attribute:"data-id"});$.post("".concat(n,"order"),{order:t})}}),$("#sortable").sortable("disable"),$("#main").on("mouseenter","#sortable.ui-sortable-disabled .item",(function(){$(this).siblings(".tooltip").addClass("active"),$(".refresh",this).addClass("active")})).on("mouseleave",".item",(function(){$(this).siblings(".tooltip").removeClass("active"),$(".refresh",this).removeClass("active")})),$("#config-buttons").on("mouseenter","a",(function(){$(".tooltip",this).addClass("active")})).on("mouseleave","a",(function(){$(".tooltip",this).removeClass("active")})),$(".searchform > form").on("submit",(function(t){"tiles"===$("#search-container select[name=provider]").val()&&t.preventDefault()})),$("#search-container").on("input","input[name=q]",(function(){var t=this.value,e=$("#sortable").children(".item-container");"tiles"===$("#search-container select[name=provider]").val()&&t.length>0?(e.hide(),e.filter((function(){return $(this).data("name").toLowerCase().includes(t.toLowerCase())})).show()):e.show()})).on("change","select[name=provider]",(function(){var t=$("#sortable").children(".item-container");if("tiles"===$(this).val()){$("#search-container button").hide();var e=$("#search-container input[name=q]").val();e.length>0?(t.hide(),t.filter((function(){return $(this).data("name").toLowerCase().includes(e.toLowerCase())})).show()):t.show()}else $("#search-container button").show(),t.show()})),$("#app").on("click","#config-button",(function(t){t.preventDefault();var e=$("#app"),n=e.hasClass("header");e.toggleClass("header"),n?($(".add-item").hide(),$(".item-edit").hide(),$("#app").removeClass("sidebar"),$("#sortable .tooltip").css("display",""),$("#sortable").sortable("disable")):($("#sortable .tooltip").css("display","none"),$("#sortable").sortable("enable"),setTimeout((function(){$(".add-item").fadeIn(),$(".item-edit").fadeIn()}),350))})).on("click","#add-item, #pin-item",(function(t){t.preventDefault(),$("#app").toggleClass("sidebar")})).on("click",".close-sidenav",(function(t){t.preventDefault(),$("#app").removeClass("sidebar")})).on("click","#test_config",(function(t){t.preventDefault();var e=$("#create input[name=url]").val(),i=$('#sapconfig input[name="config[override_url]"]').val();i.length&&""!==i&&(e=i);var s={};s.url=e,$(".config-item").each((function(){var t=$(this).data("config");s[t]=$(this).val()})),s.id=$("form[data-item-id]").data("item-id"),s.password&&s.password===o&&(s.password=""),$.post("".concat(n,"test_config"),{data:s},(function(t){alert(t)}))})),$("#pinlist").on("click","a",(function(t){t.preventDefault();var e=$(this),i=e.data("id"),o=e.data("tag");$.get("".concat(n,"items/pintoggle/").concat(i,"/true/").concat(o),(function(t){var n=$(t).filter("#sortable").html();$("#sortable").html(n),e.toggleClass("active")}))})),$("#itemform").on("submit",(function(){var t=$('input[name="config[password]"]').first();t.length>0&&t.attr("value")===o&&t.attr("value","")}))}));var focusSearch=function(t){var e=document.querySelector('input[name="q"]');e&&(t.preventDefault(),e.focus())},openFirstNonHiddenItem=function(t){if(t.target===document.querySelector('input[name="q"]')&&"tiles"===document.querySelector("#search-container select[name=provider]").value){var e=document.querySelector('#sortable section.item-container:not([style="display: none;"]) a');"href"in e&&(t.preventDefault(),window.open(e.href))}},KEY_BINDINGS={"/":focusSearch,Enter:openFirstNonHiddenItem};document.addEventListener("keydown",(function(t){try{t.key in KEY_BINDINGS&&KEY_BINDINGS[t.key](t)}catch(t){}}));var EXPORT_FILE_NAME="HeimdallExport.json",EXPORT_API_URL="api/item";function triggerFileDownload(t,e){var n=document.createElement("a"),i=new Blob([e],{type:"text/plain"});n.href=URL.createObjectURL(i),n.download=EXPORT_FILE_NAME,n.click()}var exportItems=function(t){t.preventDefault(),fetch(EXPORT_API_URL).then((function(t){return 200!==t.status&&window.alert("An error occurred while exporting..."),t.json()})).then((function(t){var e=JSON.stringify(t,null,2);triggerFileDownload(EXPORT_FILE_NAME,e)}))},exportButton=document.querySelector("#item-export");exportButton&&exportButton.addEventListener("click",exportItems);var IMPORT_API_URL="api/item",APP_LOAD_URL="appload",fetchAppDetails=function(t){return null===t?Promise.resolve({}):fetch(APP_LOAD_URL,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app:t})}).then((function(t){return t.json()})).catch((function(){return{}}))},getCSRFToken=function(){return document.querySelector('input[name="_token"]').value},postToApi=function(t,e){return fetch(IMPORT_API_URL,{method:"POST",cache:"no-cache",redirect:"follow",headers:{"Content-Type":"application/json","X-CSRF-TOKEN":e},body:JSON.stringify(t)})},mergeItemWithAppDetails=function(t,e){return{pinned:1,tags:[0],appid:t.appid,title:t.title,colour:t.colour,url:t.url,appdescription:t.appdescription?t.appdescription:e.description,website:e.website,icon:e.iconview,config:t.description?JSON.parse(t.description):null}},importItems=function(t){t.forEach((function(t){fetchAppDetails(t.appid).then((function(e){var n=mergeItemWithAppDetails(t,e),i=getCSRFToken();return postToApi(n,i)})).then((function(t){console.log(t)}))}))},readJSON=function(t){return new Promise((function(e){var n=new FileReader;n.onload=function(t){var n=t.target.result;e(JSON.parse(n))},n.readAsText(t)}))},openFileForImport=function(t){var e=t.target.files[0];e&&readJSON(e).then(importItems)},fileInput=document.querySelector("input[name='import']");fileInput&&fileInput.addEventListener("change",openFileForImport,!1); diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 3e7b1c9e..8b3143e2 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,4 +1,4 @@ { "/css/app.css": "/css/app.css?id=9a25947db63214edd4e6f459200dfa62", - "/js/app.js": "/js/app.js?id=894c631b0c521ca3e5df669b4220f77b" + "/js/app.js": "/js/app.js?id=50647209eddf7eb990cfe03fcc6652ed" } diff --git a/resources/assets/js/itemExport.js b/resources/assets/js/itemExport.js new file mode 100644 index 00000000..27b24377 --- /dev/null +++ b/resources/assets/js/itemExport.js @@ -0,0 +1,48 @@ +const EXPORT_FILE_NAME = "HeimdallExport.json"; +const EXPORT_API_URL = "api/item"; + +/** + * + * @param {string} fileName + * @param {string} data + */ +function triggerFileDownload(fileName, data) { + const a = document.createElement("a"); + + const file = new Blob([data], { + type: "text/plain", + }); + + a.href = URL.createObjectURL(file); + a.download = EXPORT_FILE_NAME; + + a.click(); +} + +/** + * + * @param {Event} event + */ +const exportItems = (event) => { + event.preventDefault(); + + fetch(EXPORT_API_URL) + .then((response) => { + if (response.status !== 200) { + window.alert("An error occurred while exporting..."); + } + + return response.json(); + }) + .then((data) => { + const exportedJson = JSON.stringify(data, null, 2); + + triggerFileDownload(EXPORT_FILE_NAME, exportedJson); + }); +}; + +const exportButton = document.querySelector("#item-export"); + +if (exportButton) { + exportButton.addEventListener("click", exportItems); +} diff --git a/resources/assets/js/itemImport.js b/resources/assets/js/itemImport.js new file mode 100644 index 00000000..5a044a54 --- /dev/null +++ b/resources/assets/js/itemImport.js @@ -0,0 +1,128 @@ +const IMPORT_API_URL = "api/item"; +const APP_LOAD_URL = "appload"; + +/** + * + * @param {string|null} appId + * @returns {Promise<{}>|Promise} + */ +const fetchAppDetails = (appId) => { + if (appId === null) { + return Promise.resolve({}); + } + + return fetch(APP_LOAD_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ app: appId }), + }) + .then((response) => response.json()) + .catch(() => ({})); +}; + +/** + * + * @returns {string} + */ +const getCSRFToken = () => { + const tokenSelector = 'input[name="_token"]'; + return document.querySelector(tokenSelector).value; +}; + +/** + * + * @param {object} data + * @param {string} csrfToken + */ +const postToApi = (data, csrfToken) => + fetch(IMPORT_API_URL, { + method: "POST", + cache: "no-cache", + redirect: "follow", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": csrfToken, + }, + body: JSON.stringify(data), + }); + +/** + * + * @param {object} item + * @param {object} appDetails + * @returns {undefined} + */ +const mergeItemWithAppDetails = (item, appDetails) => ({ + pinned: 1, + tags: [0], + + appid: item.appid, + title: item.title, + colour: item.colour, + url: item.url, + appdescription: item.appdescription + ? item.appdescription + : appDetails.description, + + website: appDetails.website, + + icon: appDetails.iconview, + config: item.description ? JSON.parse(item.description) : null, +}); + +/** + * + * @param {array} items + */ +const importItems = (items) => { + items.forEach((item) => { + fetchAppDetails(item.appid) + .then((appDetails) => { + const itemWithAppDetails = mergeItemWithAppDetails(item, appDetails); + const csrfToken = getCSRFToken(); + + return postToApi(itemWithAppDetails, csrfToken); + }) + .then((response) => { + console.log(response); + }); + }); +}; + +/** + * + * @param {Blob} file + * @returns {Promise} + */ +const readJSON = (file) => + new Promise((resolve) => { + const reader = new FileReader(); + + reader.onload = (e) => { + const contents = e.target.result; + resolve(JSON.parse(contents)); + }; + + reader.readAsText(file); + }); + +/** + * + * @param {Event} event + */ +const openFileForImport = (event) => { + const file = event.target.files[0]; + if (!file) { + return; + } + + readJSON(file).then(importItems); +}; + +const fileInput = document.querySelector("input[name='import']"); + +if (fileInput) { + fileInput.addEventListener("change", openFileForImport, false); +} diff --git a/resources/lang/en/app.php b/resources/lang/en/app.php index 49ab1df1..974f0650 100644 --- a/resources/lang/en/app.php +++ b/resources/lang/en/app.php @@ -86,6 +86,8 @@ return array ( 'delete' => 'Delete', 'optional' => 'Optional', 'restore' => 'Restore', + 'export' => 'Export', + 'import' => 'Import', 'alert.success.item_created' => 'Item created successfully', 'alert.success.item_updated' => 'Item updated successfully', 'alert.success.item_deleted' => 'Item deleted successfully', diff --git a/resources/views/items/import.blade.php b/resources/views/items/import.blade.php new file mode 100644 index 00000000..979a8292 --- /dev/null +++ b/resources/views/items/import.blade.php @@ -0,0 +1,32 @@ +@extends('layouts.app') + +@section('content') + +
+
+
{{ __('import.title') }}
+
+ + {{ __('app.buttons.cancel') }} +
+
+
+ {!! csrf_field() !!} + +
+ +
+ +
+ + +
+ + +@endsection \ No newline at end of file diff --git a/resources/views/items/list.blade.php b/resources/views/items/list.blade.php index f72e46f5..0f177d7c 100644 --- a/resources/views/items/list.blade.php +++ b/resources/views/items/list.blade.php @@ -11,6 +11,8 @@
+{{-- {{ __('import') }}--}} + {{ __('export') }} {{ __('app.buttons.downloadapps') }} {{ __('app.buttons.add') }} {{ __('app.buttons.cancel') }} diff --git a/routes/web.php b/routes/web.php index eb15c823..88dad5e6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -81,3 +81,6 @@ Route::group([ Auth::routes(); Route::get('/home', 'HomeController@index')->name('home'); + +Route::resource('api/item', 'ItemRestController'); +Route::get('import', 'ImportController')->name('items.import'); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index cdb51119..35777133 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -7,12 +7,14 @@ use Tests\TestCase; class ExampleTest extends TestCase { + use RefreshDatabase; + /** * A basic test example. * * @return void */ - public function testBasicTest() + public function test_app_loads() { $response = $this->get('/'); diff --git a/tests/Feature/ItemExportTest.php b/tests/Feature/ItemExportTest.php new file mode 100644 index 00000000..ea4a4aaf --- /dev/null +++ b/tests/Feature/ItemExportTest.php @@ -0,0 +1,81 @@ +get('api/item'); + + $response->assertJsonCount(0); + } + + public function test_returns_exactly_the_defined_fields() + { + $exampleItem = [ + "appdescription" => "Description", + "appid" => "123", + "colour" => "#000", + "description" => "Description", + "title" => "Item Title", + "url" => "http://gorczany.com/nihil-rerum-distinctio-voluptate-assumenda-accusantium-exercitationem" + ]; + + Item::factory() + ->create($exampleItem); + + $response = $this->get('api/item'); + + $response->assertExactJson([(object)$exampleItem]); + } + + public function test_returns_all_items() + { + Item::factory() + ->count(3) + ->create(); + + $response = $this->get('api/item'); + + $response->assertJsonCount(3); + } + + public function test_does_not_return_deleted_item() + { + Item::factory() + ->create([ + 'deleted_at' => Date::create('1970') + ]); + + Item::factory() + ->create(); + + $response = $this->get('api/item'); + + $response->assertJsonCount(1); + } + + public function test_does_not_return_tags() + { + Item::factory() + ->create([ + 'type' => 1 + ]); + + Item::factory() + ->create(); + + $response = $this->get('api/item'); + + $response->assertJsonCount(1); + } +} diff --git a/tests/Unit/database/seeders/SettingsSeederTest.php b/tests/Unit/database/seeders/SettingsSeederTest.php index af054eed..88532764 100644 --- a/tests/Unit/database/seeders/SettingsSeederTest.php +++ b/tests/Unit/database/seeders/SettingsSeederTest.php @@ -12,7 +12,7 @@ class SettingsSeederTest extends TestCase * * @return void */ - public function testReturnsAJSONMapWithSameAmountOfItemsAsLanguageDirectoriesPresent() + public function test_returns_a_jsonmap_with_same_amount_of_items_as_language_directories_present() { $languageDirectories = array_filter(glob(resource_path().'/lang/*'), 'is_dir'); diff --git a/tests/Unit/lang/LangTest.php b/tests/Unit/lang/LangTest.php index a02513f1..33d2105a 100644 --- a/tests/Unit/lang/LangTest.php +++ b/tests/Unit/lang/LangTest.php @@ -11,7 +11,7 @@ class LangTest extends TestCase * * @return void */ - public function testAllLanguageKeysAreDefined() + public function test_all_language_keys_are_defined() { $this->markTestSkipped('2022-11-14 Lot of keys missing. Enable this test to see them all.'); $languageDirectories = array_filter(glob(resource_path().'/lang/*'), 'is_dir'); diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 7d5047c6..eb975cac 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -18,6 +18,7 @@ return array( 'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php', 'App\\Http\\Controllers\\HomeController' => $baseDir . '/app/Http/Controllers/HomeController.php', 'App\\Http\\Controllers\\ItemController' => $baseDir . '/app/Http/Controllers/ItemController.php', + 'App\\Http\\Controllers\\ItemRestController' => $baseDir . '/app/Http/Controllers/ItemRestController.php', 'App\\Http\\Controllers\\SearchController' => $baseDir . '/app/Http/Controllers/SearchController.php', 'App\\Http\\Controllers\\SettingsController' => $baseDir . '/app/Http/Controllers/SettingsController.php', 'App\\Http\\Controllers\\TagController' => $baseDir . '/app/Http/Controllers/TagController.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 65c54196..34f2ea35 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -616,6 +616,7 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php', 'App\\Http\\Controllers\\HomeController' => __DIR__ . '/../..' . '/app/Http/Controllers/HomeController.php', 'App\\Http\\Controllers\\ItemController' => __DIR__ . '/../..' . '/app/Http/Controllers/ItemController.php', + 'App\\Http\\Controllers\\ItemRestController' => __DIR__ . '/../..' . '/app/Http/Controllers/ItemRestController.php', 'App\\Http\\Controllers\\SearchController' => __DIR__ . '/../..' . '/app/Http/Controllers/SearchController.php', 'App\\Http\\Controllers\\SettingsController' => __DIR__ . '/../..' . '/app/Http/Controllers/SettingsController.php', 'App\\Http\\Controllers\\TagController' => __DIR__ . '/../..' . '/app/Http/Controllers/TagController.php', diff --git a/webpack.mix.js b/webpack.mix.js index fdb51deb..6fa6828d 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -1,4 +1,4 @@ -let mix = require('laravel-mix'); +const mix = require("laravel-mix"); /* |-------------------------------------------------------------------------- @@ -11,12 +11,20 @@ let mix = require('laravel-mix'); | */ -mix.babel([ - //'resources/assets/js/jquery-ui.min.js', - 'resources/assets/js/huebee.js', - 'resources/assets/js/app.js', - 'resources/assets/js/keyBindings.js', - ], 'public/js/app.js') - .sass('resources/assets/sass/app.scss', 'public/css').options({ - processCssUrls: false - }).version(); +mix + .babel( + [ + // 'resources/assets/js/jquery-ui.min.js', + "resources/assets/js/huebee.js", + "resources/assets/js/app.js", + "resources/assets/js/keyBindings.js", + "resources/assets/js/itemExport.js", + "resources/assets/js/itemImport.js", + ], + "public/js/app.js" + ) + .sass("resources/assets/sass/app.scss", "public/css") + .options({ + processCssUrls: false, + }) + .version();