From 23f50e78670c44c9a650340e65749f68128db66b Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Tue, 4 Feb 2020 12:27:46 +0100 Subject: [PATCH 01/17] Updated tutorial code to 6.4.X --- README.md | 4 +- music/Module.php | 4 +- music/controller/Artist.php | 20 ++++-- music/controller/Genre.php | 20 ++++-- music/install/install.sql | 74 ++++++++++++++++++---- music/install/uninstall.sql | 6 +- music/language/en.php | 6 +- music/model/Album.php | 28 ++------- music/model/Artist.php | 99 +++++++++++------------------- music/model/Genre.php | 15 ++--- music/views/extjs3/ArtistDetail.js | 22 +++---- music/views/extjs3/ArtistDialog.js | 41 +++++++------ music/views/extjs3/ArtistGrid.js | 8 +-- music/views/extjs3/GenreCombo.js | 36 +++++------ music/views/extjs3/GenreFilter.js | 15 +++-- music/views/extjs3/MainPanel.js | 37 +++-------- music/views/extjs3/Module.js | 45 +++++++------- 17 files changed, 240 insertions(+), 240 deletions(-) diff --git a/README.md b/README.md index ee8c4aa..b5d1430 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ This code belongs to the Group-Office developer tutorial: https://groupoffice.readthedocs.io/en/latest/developer/index.html -Clone this as "tutorial" insde the go/modules folder: +Clone this as "tutorial" inside the go/modules folder: ``` cd go/modules -clone https://github.com/Intermesh/groupoffice-tutorial.git tutorial +git clone https://github.com/Intermesh/groupoffice-tutorial.git tutorial ```` \ No newline at end of file diff --git a/music/Module.php b/music/Module.php index c46f1c7..82ce05e 100644 --- a/music/Module.php +++ b/music/Module.php @@ -4,8 +4,8 @@ use go\core; /** - * @copyright (c) 2019, Intermesh BV http://www.intermesh.nl - * @author Merijn Schering + * @copyright (c) 2020, Intermesh BV http://www.intermesh.nl + * @author Joachim van de Haterd * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ class Module extends core\Module { diff --git a/music/controller/Artist.php b/music/controller/Artist.php index 4fe5ca1..e3e1516 100644 --- a/music/controller/Artist.php +++ b/music/controller/Artist.php @@ -1,7 +1,10 @@ defaultSet($params); } - /** * Handles the Artist entity's Artist/changes command - * * @param array $params + * @return mixed + * @throws InvalidArguments * @see https://jmap.io/spec-core.html#/changes */ public function changes($params) { diff --git a/music/controller/Genre.php b/music/controller/Genre.php index 552d83b..cf87fc7 100644 --- a/music/controller/Genre.php +++ b/music/controller/Genre.php @@ -1,7 +1,10 @@ defaultSet($params); } - /** * Handles the Genre entity's Genre/changes command - * * @param array $params + * @return mixed + * @throws InvalidArguments * @see https://jmap.io/spec-core.html#/changes */ public function changes($params) { diff --git a/music/install/install.sql b/music/install/install.sql index 57ed277..82837d3 100644 --- a/music/install/install.sql +++ b/music/install/install.sql @@ -1,3 +1,13 @@ +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET AUTOCOMMIT = 0; +START TRANSACTION; +SET time_zone = "+00:00"; + + +-- +-- Table structure for table `music_album` +-- + CREATE TABLE `music_album` ( `id` int(11) NOT NULL, `artistId` int(11) NOT NULL, @@ -6,6 +16,20 @@ CREATE TABLE `music_album` ( `genreId` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Dumping data for table `music_album` +-- + +INSERT INTO `music_album` (`id`, `artistId`, `name`, `releaseDate`, `genreId`) VALUES +(1, 3, 'The Doors', '1967-01-04', 2), +(1, 3, 'Strange Days', '1967-09-25', 2); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `music_artist` +-- + CREATE TABLE `music_artist` ( `id` int(11) NOT NULL, `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, @@ -16,45 +40,73 @@ CREATE TABLE `music_artist` ( `modifiedBy` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Dumping data for table `music_artist` +-- + +INSERT INTO `music_artist` (`id`, `name`, `photo`, `createdAt`, `modifiedAt`, `createdBy`, `modifiedBy`) VALUES +(1, 'The Doors', NULL, '2020-02-03 13:05:25', '2020-02-03 13:05:25', 1, 1); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `music_genre` +-- CREATE TABLE `music_genre` ( `id` int(11) NOT NULL, `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Dumping data for table `music_genre` +-- + INSERT INTO `music_genre` (`id`, `name`) VALUES (1, 'Pop'), (2, 'Rock'), (3, 'Blues'), (4, 'Jazz'); +-- +-- Indexes for dumped tables +-- +-- +-- Indexes for table `music_album` +-- ALTER TABLE `music_album` ADD PRIMARY KEY (`id`), ADD KEY `artistId` (`artistId`), ADD KEY `genreId` (`genreId`); +-- +-- Indexes for table `music_artist` +-- ALTER TABLE `music_artist` ADD PRIMARY KEY (`id`), ADD KEY `photo` (`photo`); +-- +-- Indexes for table `music_genre` +-- ALTER TABLE `music_genre` ADD PRIMARY KEY (`id`); +-- +-- Constraints for dumped tables +-- -ALTER TABLE `music_album` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=14; - -ALTER TABLE `music_artist` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; - -ALTER TABLE `music_genre` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5; - - +-- +-- Constraints for table `music_album` +-- ALTER TABLE `music_album` ADD CONSTRAINT `music_album_ibfk_1` FOREIGN KEY (`artistId`) REFERENCES `music_artist` (`id`) ON DELETE CASCADE, ADD CONSTRAINT `music_album_ibfk_2` FOREIGN KEY (`genreId`) REFERENCES `music_genre` (`id`); +-- +-- Constraints for table `music_artist` +-- ALTER TABLE `music_artist` - ADD CONSTRAINT `music_artist_ibfk_1` FOREIGN KEY (`photo`) REFERENCES `core_blob` (`id`); \ No newline at end of file + ADD CONSTRAINT `music_artist_ibfk_1` FOREIGN KEY (`photo`) REFERENCES `core_blob` (`id`); +COMMIT; diff --git a/music/install/uninstall.sql b/music/install/uninstall.sql index 70189a0..0aaa69d 100644 --- a/music/install/uninstall.sql +++ b/music/install/uninstall.sql @@ -1,3 +1,3 @@ -DROP TABLE IF EXISTS music_album; -DROP TABLE IF EXISTS music_artist; -DROP TABLE IF EXISTS music_genre; \ No newline at end of file +DROP TABLE IF EXISTS `music_album`; +DROP TABLE IF EXISTS `music_artist`; +DROP TABLE IF EXISTS `music_genre`; \ No newline at end of file diff --git a/music/language/en.php b/music/language/en.php index b7d4fe9..f5ee0b4 100644 --- a/music/language/en.php +++ b/music/language/en.php @@ -1,5 +1,5 @@ 'Music', - 'description' => 'Simple module for the Group-Office tutorial' -]; + 'name' => 'Music', + 'description' => 'Simple tutorial module for Group-Office', +]; \ No newline at end of file diff --git a/music/model/Album.php b/music/model/Album.php index f329dcd..8cdf08f 100644 --- a/music/model/Album.php +++ b/music/model/Album.php @@ -6,41 +6,25 @@ /** * Album model * - * @copyright (c) 2019, Intermesh BV http://www.intermesh.nl + * @copyright (c) 2020, Intermesh BV http://www.intermesh.nl * @author Merijn Schering * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ - class Album extends Property { - /** - * - * @var int - */ + /** @var int */ public $id; - /** - * - * @var int - */ + /** @var int */ public $artistId; - /** - * - * @var string - */ + /** @var string */ public $name; - /** - * - * @var \go\core\util\DateTime - */ + /** @var \go\core\util\DateTime */ public $releaseDate; - /** - * - * @var int - */ + /** @var int */ public $genreId; protected static function defineMapping() { diff --git a/music/model/Artist.php b/music/model/Artist.php index afa733c..889b5d5 100644 --- a/music/model/Artist.php +++ b/music/model/Artist.php @@ -1,100 +1,71 @@ * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ class Artist extends Entity { - - /** - * - * @var int - */ + + /** @var int */ public $id; - /** - * - * @var string - */ + /** @var string */ public $name; - /** - * - * @var string - */ + /** @var string */ public $photo; - /** - * - * @var DateTime - */ + /** @var \go\core\util\DateTime */ public $createdAt; - /** - * - * @var DateTime - */ + /** @var \go\core\util\DateTime */ public $modifiedAt; - /** - * - * @var int - */ + /** @var int */ public $createdBy; - /** - * - * @var int - */ + /** @var int */ public $modifiedBy; - - /** - * The albums created by the artist - * - * @var Album[] - */ + + /** @var Album[] */ public $albums; protected static function defineMapping() { return parent::defineMapping() - ->addTable("music_artist", "artist") - ->addArray('albums', Album::class, ['id' => 'artistId']); + ->addTable("music_artist", "artist") + ->addArray('albums', Album::class, ['id' => 'artistId']); } /** - * This function returns the columns to search when using the "text" filter. - */ - public static function textFilterColumns() { - return ['name']; - } + * This function returns the columns to search when using the "text" filter. + */ + public static function textFilterColumns() { + return ['name']; + } + /** - * Defines JMAP filters + * Define JMAP filters * - * Adds the 'genres' filter which can be an array of genre id's. - * - * @link https://jmap.io/spec-core.html#/query - * - * @return Filters + * Add the 'genres' filter which can be an array of genre IDs + * @return \go\core\orm\Filters + * @throws \Exception */ - protected static function defineFilters() { + protected static function defineFilters() + { return parent::defineFilters() - ->add('genres', function (\go\core\db\Criteria $criteria, $value, \go\core\orm\Query $query, array $filter) { - if (!empty($value)) { - $query->join('music_album', 'album', 'album.artistId = artist.id') - ->groupBy(['artist.id']) // group the results by id to filter out duplicates because of the join - ->where(['album.genreId' => $value]); - } - }); + ->add('genres', function(\go\core\db\Criteria $criteria, $value, \go\core\orm\Query $query, array $filter){ + if(!empty($value)) { + $query->join('music_album', 'album', 'album.artistId = artist.id') + ->groupBy(['artist.id']) + ->where(['album.genreId' => $value]); + } + }); } - -} +} \ No newline at end of file diff --git a/music/model/Genre.php b/music/model/Genre.php index 4226ec4..3842ccd 100644 --- a/music/model/Genre.php +++ b/music/model/Genre.php @@ -1,28 +1,21 @@ * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ - class Genre extends Entity { - /** - * - * @var int - */ + /** @var int */ public $id; - /** - * - * @var string - */ + /** @var string */ public $name; protected static function defineMapping() { diff --git a/music/views/extjs3/ArtistDetail.js b/music/views/extjs3/ArtistDetail.js index fea634d..5a10de7 100644 --- a/music/views/extjs3/ArtistDetail.js +++ b/music/views/extjs3/ArtistDetail.js @@ -29,7 +29,7 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { xtype: "box", cls: "content", tpl: new Ext.XTemplate('
\ -
', +
', { getCls: function (isOrganization) { return isOrganization ? "organization" : ""; @@ -46,13 +46,13 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { title: t("Albums"), xtype: "panel", tpl: '
\ - \ -

album\ - {name}\ - \ -

\ -
\ -
' + \ +

album\ + {name}\ + \ +

\ +
\ + ' } ] }); @@ -96,7 +96,7 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { iconCls: "ic-print", text: t("Print"), handler: function () { - this.body.print({ title: this.data.name }); + this.body.print({title: this.data.name}); }, scope: this }, @@ -110,7 +110,7 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { if (btn != "yes") { return; } - this.entityStore.set({ destroy: [this.currentId] }); + this.entityStore.set({destroy: [this.currentId]}); }, this); }, scope: this @@ -126,4 +126,4 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { return new Ext.Toolbar(tbarCfg); } -}); +}); \ No newline at end of file diff --git a/music/views/extjs3/ArtistDialog.js b/music/views/extjs3/ArtistDialog.js index 2f664bf..88e560d 100644 --- a/music/views/extjs3/ArtistDialog.js +++ b/music/views/extjs3/ArtistDialog.js @@ -12,22 +12,22 @@ go.modules.tutorial.music.ArtistDialog = Ext.extend(go.form.Dialog, { // return an array of form items here. initFormItems: function () { return [{ - // it's recommended to wrap all fields in field sets for consistent style. - xtype: 'fieldset', - title: t("Artist information"), - items: [ - this.avatarComp = new go.form.ImageField({ - name: 'photo' - }), + // it's recommended to wrap all fields in field sets for consistent style. + xtype: 'fieldset', + title: t("Artist information"), + items: [ + this.avatarComp = new go.form.ImageField({ + name: 'photo' + }), - { - xtype: 'textfield', - name: 'name', - fieldLabel: t("Name"), - anchor: '100%', - allowBlank: false - }] - }, + { + xtype: 'textfield', + name: 'name', + fieldLabel: t("Name"), + anchor: '100%', + allowBlank: false + }] + }, { xtype: "fieldset", @@ -47,14 +47,15 @@ go.modules.tutorial.music.ArtistDialog = Ext.extend(go.form.Dialog, { //the itemCfg is used to create a component for each "album" in the array. itemCfg: { + layout: "form", defaults: { anchor: "100%" }, items: [{ - xtype: "textfield", - fieldLabel: t("Name"), - name: "name" - }, + xtype: "textfield", + fieldLabel: t("Name"), + name: "name" + }, { xtype: "datefield", @@ -72,4 +73,4 @@ go.modules.tutorial.music.ArtistDialog = Ext.extend(go.form.Dialog, { } ]; } -}); +}); \ No newline at end of file diff --git a/music/views/extjs3/ArtistGrid.js b/music/views/extjs3/ArtistGrid.js index 6a8b960..70089db 100644 --- a/music/views/extjs3/ArtistGrid.js +++ b/music/views/extjs3/ArtistGrid.js @@ -49,9 +49,9 @@ go.modules.tutorial.music.ArtistGrid = Ext.extend(go.grid.GridPanel, { var style = record.data.photo ? 'background-image: url(' + go.Jmap.downloadUrl(record.data.photo) + ')"' : ''; return '
\ -
\ -
' + record.get('name') + '
\ -
'; +
\ +
' + record.get('name') + '
\ + '; } }, { @@ -107,4 +107,4 @@ go.modules.tutorial.music.ArtistGrid = Ext.extend(go.grid.GridPanel, { go.modules.tutorial.music.ArtistGrid.superclass.initComponent.call(this); } -}); +}); \ No newline at end of file diff --git a/music/views/extjs3/GenreCombo.js b/music/views/extjs3/GenreCombo.js index b37e761..88eca83 100644 --- a/music/views/extjs3/GenreCombo.js +++ b/music/views/extjs3/GenreCombo.js @@ -1,22 +1,22 @@ go.modules.tutorial.music.GenreCombo = Ext.extend(go.form.ComboBox, { - fieldLabel: t("Genre"), - hiddenName: 'genreId', - anchor: '100%', - emptyText: t("Please select..."), - pageSize: 50, - valueField: 'id', - displayField: 'name', - triggerAction: 'all', - editable: true, - selectOnFocus: true, - forceSelection: true, - allowBlank: false, - store: { - xtype: "gostore", - fields: ['id', 'name'], - entityStore: "Genre" - } + fieldLabel: t("Genre"), + hiddenName: 'genreId', + anchor: '100%', + emptyText: t("Please select..."), + pageSize: 50, + valueField: 'id', + displayField: 'name', + triggerAction: 'all', + editable: true, + selectOnFocus: true, + forceSelection: true, + allowBlank: false, + store: { + xtype: "gostore", + fields: ['id', 'name'], + entityStore: "Genre" + } }); // Register an xtype so we can use the component easily. -Ext.reg("genrecombo", go.modules.tutorial.music.GenreCombo); +Ext.reg("genrecombo", go.modules.tutorial.music.GenreCombo); \ No newline at end of file diff --git a/music/views/extjs3/GenreFilter.js b/music/views/extjs3/GenreFilter.js index 33342dc..1e09c5a 100644 --- a/music/views/extjs3/GenreFilter.js +++ b/music/views/extjs3/GenreFilter.js @@ -7,8 +7,6 @@ go.modules.tutorial.music.GenreFilter = Ext.extend(go.grid.GridPanel, { //This component is going to be the side navigation cls: 'go-sidenav', - hideHeaders: true, - initComponent: function () { // Row actions is a special grid column with an actions menu in it. @@ -78,8 +76,8 @@ go.modules.tutorial.music.GenreFilter = Ext.extend(go.grid.GridPanel, { keepSelection: true, actions: [{ - iconCls: 'ic-more-vert' - }] + iconCls: 'ic-more-vert' + }] }); actions.on({ @@ -93,6 +91,7 @@ go.modules.tutorial.music.GenreFilter = Ext.extend(go.grid.GridPanel, { }, + showMoreMenu: function (record, e) { if (!this.moreMenu) { this.moreMenu = new Ext.menu.Menu({ @@ -117,7 +116,7 @@ go.modules.tutorial.music.GenreFilter = Ext.extend(go.grid.GridPanel, { if (btn != "yes") { return; } - go.Db.store("Genre").set({destroy: [this.moreMenu.record.id]}); + go.Stores.get("Genre").set({destroy: [this.moreMenu.record.id]}); }, this); }, scope: this @@ -126,11 +125,11 @@ go.modules.tutorial.music.GenreFilter = Ext.extend(go.grid.GridPanel, { }) } - this.moreMenu.getComponent("edit").setDisabled(record.get("permissionLevel") < go.permissionLevels.manage); - this.moreMenu.getComponent("delete").setDisabled(record.get("permissionLevel") < go.permissionLevels.manage); + this.moreMenu.getComponent("edit").setDisabled(record.get("permissionLevel") < GO.permissionLevels.manage); + this.moreMenu.getComponent("delete").setDisabled(record.get("permissionLevel") < GO.permissionLevels.manage); this.moreMenu.record = record; this.moreMenu.showAt(e.getXY()); } -}); +}); \ No newline at end of file diff --git a/music/views/extjs3/MainPanel.js b/music/views/extjs3/MainPanel.js index 56550e9..2d2c32d 100644 --- a/music/views/extjs3/MainPanel.js +++ b/music/views/extjs3/MainPanel.js @@ -19,9 +19,9 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { split: true, tbar: [{ - xtype: "tbtitle", - text: t("Genres") - }, + xtype: "tbtitle", + text: t("Genres") + }, '->', //add back button for smaller screens @@ -74,6 +74,7 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { }), { iconCls: 'ic-more-vert', + tooltip: t("More options"), menu: [ { itemId: "delete", @@ -90,7 +91,6 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { listeners: { rowdblclick: this.onGridDblClick, - keypress: this.onGridKeyPress, scope: this } }); @@ -110,7 +110,7 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { cls: 'go-narrow', iconCls: "ic-arrow-back", handler: function () { - go.Router.goto("music"); + this.westPanel.show(); }, scope: this }] @@ -151,9 +151,9 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { onGenreFilterChange: function (sm) { var selectedRecords = sm.getSelections(), - ids = selectedRecords.column('id'); //column is a special GO method that get's all the id's from the records in an array. + ids = selectedRecords.column('id'); //column is a special GO method that get's all the id's from the records in an array. - this.artistGrid.store.baseParams.filter.genres = ids; + this.artistGrid.store.setFilter('genres', {genres: ids}); this.artistGrid.store.load(); }, @@ -169,33 +169,12 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { //check permissions var record = grid.getStore().getAt(rowIndex); - if (record.get('permissionLevel') < go.permissionLevels.write) { + if (record.get('permissionLevel') < GO.permissionLevels.write) { return; } // Show dialog var dlg = new go.modules.tutorial.music.ArtistDialog(); dlg.load(record.id).show(); - }, - - // Fires when enter is pressed and a grid row is focussed - onGridKeyPress: function (e) { - if (e.keyCode != e.ENTER) { - return; - } - - var record = this.artistGrid.getSelectionModel().getSelected(); - if (!record) { - return; - } - - if (record.get('permissionLevel') < go.permissionLevels.write) { - return; - } - - var dlg = new go.modules.tutorial.music.ArtistDialog(); - dlg.load(record.id).show(); - } - }); \ No newline at end of file diff --git a/music/views/extjs3/Module.js b/music/views/extjs3/Module.js index 46cc24a..8ec4fad 100644 --- a/music/views/extjs3/Module.js +++ b/music/views/extjs3/Module.js @@ -1,27 +1,28 @@ go.Modules.register("tutorial", "music", { - mainPanel: "go.modules.tutorial.music.MainPanel", + mainPanel: "go.modules.tutorial.music.MainPanel", - //The title is shown in the menu and tab bar - title: t("Music"), + //The title is shown in the menu and tab bar + title: t("Music"), - //All module entities must be defined here. Stores will be created for them. - entities: [ - "Genre", - { - name: "Artist", - relations: { - creator: {store: "User", fk: "createdBy"}, - modifier: {store: "User", fk: "createdBy"}, + //All module entities must be defined here. Stores will be created for them. + entities: [ + "Genre", + { + name: "Artist", + relations: { + creator: {store: "User", fk: "createdBy"}, + modifier: {store: "User", fk: "createdBy"}, - // 'albums' is a property of artist and has a nested relation. - albums: { - genre: {store: "Genre", fk: "genreId"} - } - } - } - ], + // 'albums' is a property of artist and has a nested relation. + albums: { + genre: {store: "Genre", fk: "genreId"} + } + } + } + ], - //Put code to initialize the module here after the user is authenticated - //and has access to the module. - initModule: function () {} -}); + //Put code to initialize the module here after the user is authenticated + //and has access to the module. + initModule: function () { + } +}); \ No newline at end of file From d8ff48c7429759ac046abd8be711b3b6b549a34b Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Wed, 5 Feb 2020 17:06:02 +0100 Subject: [PATCH 02/17] Updated tutorials code --- music/install/updates.php | 6 + music/model/Album.php | 5 +- music/model/Artist.php | 6 +- music/views/extjs3/ArtistDetail.js | 3 + music/views/extjs3/GenreFilter.js | 3 - music/views/extjs3/MainPanel.js | 187 ++++++++++++++++++----------- music/views/extjs3/Module.js | 5 + 7 files changed, 141 insertions(+), 74 deletions(-) diff --git a/music/install/updates.php b/music/install/updates.php index c8bdbdf..acea26e 100644 --- a/music/install/updates.php +++ b/music/install/updates.php @@ -2,3 +2,9 @@ $updates = []; +$updates['202002041445'][] = <<<'EOT' +CREATE TABLE IF NOT EXISTS `music_artist_custom_fields` + ( id INT(11) NOT NULL PRIMARY KEY, + CONSTRAINT `music_artist_custom_fields_ibfk_1` FOREIGN KEY(id) REFERENCES music_artist (id) + ON DELETE CASCADE ON UPDATE RESTRICT ) ENGINE = INNODB; +EOT; diff --git a/music/model/Album.php b/music/model/Album.php index 8cdf08f..20f30ce 100644 --- a/music/model/Album.php +++ b/music/model/Album.php @@ -2,7 +2,8 @@ namespace go\modules\tutorial\music\model; use go\core\orm\Property; - +//use go\core\orm\CustomFieldsTrait; + /** * Album model * @@ -11,7 +12,7 @@ * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ class Album extends Property { - +// use CustomFieldsTrait; /** @var int */ public $id; diff --git a/music/model/Artist.php b/music/model/Artist.php index 889b5d5..1486287 100644 --- a/music/model/Artist.php +++ b/music/model/Artist.php @@ -2,7 +2,8 @@ namespace go\modules\tutorial\music\model; use go\core\jmap\Entity; - +use go\core\orm\CustomFieldsTrait; + /** * Artist model * @@ -11,7 +12,8 @@ * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ class Artist extends Entity { - + use CustomFieldsTrait; + /** @var int */ public $id; diff --git a/music/views/extjs3/ArtistDetail.js b/music/views/extjs3/ArtistDetail.js index 5a10de7..24c6e99 100644 --- a/music/views/extjs3/ArtistDetail.js +++ b/music/views/extjs3/ArtistDetail.js @@ -59,6 +59,9 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { go.modules.tutorial.music.ArtistDetail.superclass.initComponent.call(this); + this.addCustomFields(); + // this.add(new go.detail.CreateModifyPanel()); + }, onLoad: function () { diff --git a/music/views/extjs3/GenreFilter.js b/music/views/extjs3/GenreFilter.js index 1e09c5a..2ccb378 100644 --- a/music/views/extjs3/GenreFilter.js +++ b/music/views/extjs3/GenreFilter.js @@ -4,9 +4,6 @@ go.modules.tutorial.music.GenreFilter = Ext.extend(go.grid.GridPanel, { autoFill: true }, - //This component is going to be the side navigation - cls: 'go-sidenav', - initComponent: function () { // Row actions is a special grid column with an actions menu in it. diff --git a/music/views/extjs3/MainPanel.js b/music/views/extjs3/MainPanel.js index 2d2c32d..269274d 100644 --- a/music/views/extjs3/MainPanel.js +++ b/music/views/extjs3/MainPanel.js @@ -10,35 +10,77 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { initComponent: function () { - //create the genre filter component - this.genreFilter = new go.modules.tutorial.music.GenreFilter({ - region: "west", - width: dp(300), + this.createArtistGrid(); - //render a split bar for resizing - split: true, + // Every entity automatically configures a route. Route to the entity when selecting it in the grid. + this.artistGrid.on('navigate', function (grid, rowIndex, record) { + go.Router.goto("artist/" + record.id); + }, this); - tbar: [{ - xtype: "tbtitle", - text: t("Genres") + this.sidePanel = new Ext.Panel({ + layout: 'anchor', + defaults: { + anchor: '100%' }, - '->', - - //add back button for smaller screens + width: dp(300), + cls: 'go-sidenav', + region: "west", + split: true, + autoScroll: true, + items: [ + this.createGenreFilter(), + this.createFilterPanel() + ] + }); + // Create artist detail component + this.artistDetail = new go.modules.tutorial.music.ArtistDetail({ + region: "center", + tbar: [ + //add a back button for small screens { - //this class will hide it on larger screens + // this class will hide the button on large screens cls: 'go-narrow', - iconCls: "ic-arrow-forward", - tooltip: t("Artists"), + iconCls: "ic-arrow-back", handler: function () { - this.artistGrid.show(); + this.westPanel.show(); }, scope: this - } + }] + }); + + //Wrap the grids into another panel with responsive layout for the 3 column responsive layout to work. + this.westPanel = new Ext.Panel({ + region: "west", + layout: "responsive", + stateId: "go-music-west", + split: true, + width: dp(800), + narrowWidth: dp(500), //this will only work for panels inside another panel with layout=responsive. Not ideal but at the moment the only way I could make it work + items: [ + this.artistGrid, //first item is shown as default in narrow mode. + this.sidePanel ] }); - //Create the artist grid + //add the components to the main panel's items. + this.items = [ + this.westPanel, //first is default in narrow mode + this.artistDetail + ]; + + // Call the parent class' initComponent + go.modules.tutorial.music.MainPanel.superclass.initComponent.call(this); + + //Attach lister to changes of the filter selection. + //add buffer because it clears selection first and this would cause it to fire twice + this.genreFilter.getSelectionModel().on('selectionchange', this.onGenreFilterChange, this, {buffer: 1}); + + // Attach listener for running the module + this.on("afterrender", this.runModule, this); + }, + + + createArtistGrid: function() { this.artistGrid = new go.modules.tutorial.music.ArtistGrid({ region: "center", @@ -94,67 +136,78 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { scope: this } }); + return this.artistGrid; + }, - // Every entity automatically configures a route. Route to the entity when selecting it in the grid. - this.artistGrid.on('navigate', function (grid, rowIndex, record) { - go.Router.goto("artist/" + record.id); - }, this); + // Fired when the Genre filter selection changes + onGenreFilterChange: function (sm) { - // Create artist detail component - this.artistDetail = new go.modules.tutorial.music.ArtistDetail({ - region: "center", - tbar: [ - //add a back button for small screens + var selectedRecords = sm.getSelections(), + ids = selectedRecords.column('id'); //column is a special GO method that get's all the id's from the records in an array. + + this.artistGrid.store.setFilter('genres', {genres: ids}); + this.artistGrid.store.load(); + }, + + createGenreFilter: function() { + this.genreFilter = new go.modules.tutorial.music.GenreFilter({ + region: "west", + width: dp(300), + autoHeight: true, + + //render a split bar for resizing + split: true, + + tbar: [{ + xtype: "tbtitle", + text: t("Genres") + }, + '->', + + //add back button for smaller screens { - // this class will hide the button on large screens + //this class will hide it on larger screens cls: 'go-narrow', - iconCls: "ic-arrow-back", + iconCls: "ic-arrow-forward", + tooltip: t("Artists"), handler: function () { - this.westPanel.show(); + this.artistGrid.show(); }, scope: this - }] + } + ] }); + return this.genreFilter; + }, - //Wrap the grids into another panel with responsive layout for the 3 column responsive layout to work. - this.westPanel = new Ext.Panel({ - region: "west", - layout: "responsive", - stateId: "go-music-west", - split: true, - width: dp(800), - narrowWidth: dp(500), //this will only work for panels inside another panel with layout=responsive. Not ideal but at the moment the only way I could make it work + createFilterPanel: function() { + return new Ext.Panel({ + autoHeight: true, + tbar: [ + { + xtype: 'tbtitle', + text: t("Filters") + }, + '->', + { + xtype: "button", + iconCls: "ic-add", + handler: function() { + var dlg = new go.filter.FilterDialog({ + entity: "Artist" + }); + dlg.show(); + }, + scope: this + } + ], items: [ - this.artistGrid, //first item is shown as default in narrow mode. - this.genreFilter + this.filterGrid = new go.filter.FilterGrid({ + filterStore: this.artistGrid.store, + entity: "Artist" + }) ] }); - - //add the components to the main panel's items. - this.items = [ - this.westPanel, //first is default in narrow mode - this.artistDetail - ]; - - // Call the parent class' initComponent - go.modules.tutorial.music.MainPanel.superclass.initComponent.call(this); - - //Attach lister to changes of the filter selection. - //add buffer because it clears selection first and this would cause it to fire twice - this.genreFilter.getSelectionModel().on('selectionchange', this.onGenreFilterChange, this, {buffer: 1}); - - // Attach listener for running the module - this.on("afterrender", this.runModule, this); - }, - - // Fired when the Genre filter selection changes - onGenreFilterChange: function (sm) { - - var selectedRecords = sm.getSelections(), - ids = selectedRecords.column('id'); //column is a special GO method that get's all the id's from the records in an array. - - this.artistGrid.store.setFilter('genres', {genres: ids}); - this.artistGrid.store.load(); }, // Fired when the module panel is rendered. diff --git a/music/views/extjs3/Module.js b/music/views/extjs3/Module.js index 8ec4fad..23b44ce 100644 --- a/music/views/extjs3/Module.js +++ b/music/views/extjs3/Module.js @@ -19,6 +19,11 @@ go.Modules.register("tutorial", "music", { } } } + // }, + // { + // name: "Albums", + // relations: {store: "Genre", fk: "genreId"} + // } ], //Put code to initialize the module here after the user is authenticated From dc1080f8b4c72ca6cf08d9f010b31aac357e7ae0 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Thu, 6 Feb 2020 13:55:50 +0100 Subject: [PATCH 03/17] Updated code for custom CSS --- music/controller/Artist.php | 1 - music/language/en.php | 1 + music/model/Artist.php | 19 +++++++++++++++++-- music/views/extjs3/ArtistDetail.js | 5 +---- music/views/extjs3/ArtistGrid.js | 10 +++++++++- music/views/extjs3/Module.js | 2 +- .../extjs3/themes/default/src/style.scss | 7 +++++++ music/views/extjs3/themes/default/style.css | 9 +++++++++ .../views/extjs3/themes/default/style.css.map | 1 + 9 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 music/views/extjs3/themes/default/src/style.scss create mode 100644 music/views/extjs3/themes/default/style.css.map diff --git a/music/controller/Artist.php b/music/controller/Artist.php index e3e1516..db87af8 100644 --- a/music/controller/Artist.php +++ b/music/controller/Artist.php @@ -1,7 +1,6 @@ 'Music', 'description' => 'Simple tutorial module for Group-Office', + 'album_count' => 'Albums', ]; \ No newline at end of file diff --git a/music/model/Artist.php b/music/model/Artist.php index 1486287..6f3593d 100644 --- a/music/model/Artist.php +++ b/music/model/Artist.php @@ -1,6 +1,7 @@ addTable("music_artist", "artist") @@ -70,4 +74,15 @@ protected static function defineFilters() } }); } + + /** + * The album count is simply the number of albums as per the artist-album relation + * + * @return int + */ + public function getAlbumcount() :int + { + return count($this->albums); + } + } \ No newline at end of file diff --git a/music/views/extjs3/ArtistDetail.js b/music/views/extjs3/ArtistDetail.js index 24c6e99..9a2e775 100644 --- a/music/views/extjs3/ArtistDetail.js +++ b/music/views/extjs3/ArtistDetail.js @@ -29,11 +29,8 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { xtype: "box", cls: "content", tpl: new Ext.XTemplate('
\ -
', +
', { - getCls: function (isOrganization) { - return isOrganization ? "organization" : ""; - }, getStyle: function (photoBlobId) { return photoBlobId ? 'background-image: url(' + go.Jmap.downloadUrl(photoBlobId) + ')"' : ""; } diff --git a/music/views/extjs3/ArtistGrid.js b/music/views/extjs3/ArtistGrid.js index 70089db..439ffd3 100644 --- a/music/views/extjs3/ArtistGrid.js +++ b/music/views/extjs3/ArtistGrid.js @@ -18,7 +18,8 @@ go.modules.tutorial.music.ArtistGrid = Ext.extend(go.grid.GridPanel, { // Every entity has permission levels. go.permissionLevels.read, write, // writeAndDelete and manage - 'permissionLevel' + 'permissionLevel', + 'albumcount' ], // The connected entity store. When Artists are changed the store will @@ -54,6 +55,13 @@ go.modules.tutorial.music.ArtistGrid = Ext.extend(go.grid.GridPanel, { '; } }, + { + id: 'albumcount', + sortable: false, + header: t('album_count','music','tutorial'), + dataIndex: 'albumcount', + width: dp(80) + }, { xtype: "datecolumn", id: 'createdAt', diff --git a/music/views/extjs3/Module.js b/music/views/extjs3/Module.js index 23b44ce..99ae678 100644 --- a/music/views/extjs3/Module.js +++ b/music/views/extjs3/Module.js @@ -21,7 +21,7 @@ go.Modules.register("tutorial", "music", { } // }, // { - // name: "Albums", + // name: "Album", // relations: {store: "Genre", fk: "genreId"} // } ], diff --git a/music/views/extjs3/themes/default/src/style.scss b/music/views/extjs3/themes/default/src/style.scss new file mode 100644 index 0000000..aaa3e59 --- /dev/null +++ b/music/views/extjs3/themes/default/src/style.scss @@ -0,0 +1,7 @@ +.go-detail-view-avatar { + text-align:center; + & > .avatar { + width: 120px; + height: 120px; + } +} \ No newline at end of file diff --git a/music/views/extjs3/themes/default/style.css b/music/views/extjs3/themes/default/style.css index e69de29..d766e1c 100644 --- a/music/views/extjs3/themes/default/style.css +++ b/music/views/extjs3/themes/default/style.css @@ -0,0 +1,9 @@ +.go-detail-view-avatar { + text-align: center; +} +.go-detail-view-avatar > .avatar { + width: 120px; + height: 120px; +} + +/*# sourceMappingURL=style.css.map */ diff --git a/music/views/extjs3/themes/default/style.css.map b/music/views/extjs3/themes/default/style.css.map new file mode 100644 index 0000000..47889f2 --- /dev/null +++ b/music/views/extjs3/themes/default/style.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["src/style.scss"],"names":[],"mappings":"AAAA;EACE;;AACA;EACE;EACA","file":"style.css"} \ No newline at end of file From 8687861b2d62cbb7c7a11db016fa9c9421d4962e Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Mon, 17 Feb 2020 10:42:17 +0100 Subject: [PATCH 04/17] Updated documentation up to ACL chapter --- music/controller/Review.php | 70 ++++++++++ music/install/updates.php | 30 +++++ music/model/Album.php | 8 +- music/model/Artist.php | 15 +-- music/model/Review.php | 50 ++++++++ music/views/extjs3/ArtistDetail.js | 65 +++++++++- music/views/extjs3/ArtistDialog.js | 4 + music/views/extjs3/MainPanel.js | 2 +- music/views/extjs3/Module.js | 15 ++- music/views/extjs3/ReviewDialog.js | 61 +++++++++ music/views/extjs3/ReviewsModal.js | 197 +++++++++++++++++++++++++++++ music/views/extjs3/scripts.txt | 4 +- 12 files changed, 491 insertions(+), 30 deletions(-) create mode 100644 music/controller/Review.php create mode 100644 music/model/Review.php create mode 100644 music/views/extjs3/ReviewDialog.js create mode 100644 music/views/extjs3/ReviewsModal.js diff --git a/music/controller/Review.php b/music/controller/Review.php new file mode 100644 index 0000000..880c933 --- /dev/null +++ b/music/controller/Review.php @@ -0,0 +1,70 @@ +defaultQuery($params); + } + + /** + * Handles the Artist entity's Artist/get command + * + * @param array $params + * @return array + * @throws InvalidArguments + * @see https://jmap.io/spec-core.html#/get + */ + public function get($params) { + return $this->defaultGet($params); + } + + /** + * Handles the Artist entity's Artist/set command + * + * @see https://jmap.io/spec-core.html#/set + * @param array $params + * @return array + * @throws StateMismatch + * @throws InvalidArguments + */ + public function set($params) { + return $this->defaultSet($params); + } + + /** + * Handles the Artist entity's Artist/changes command + * @param array $params + * @return mixed + * @throws InvalidArguments + * @see https://jmap.io/spec-core.html#/changes + */ + public function changes($params) { + return $this->defaultChanges($params); + } +} diff --git a/music/install/updates.php b/music/install/updates.php index acea26e..ad02036 100644 --- a/music/install/updates.php +++ b/music/install/updates.php @@ -8,3 +8,33 @@ CONSTRAINT `music_artist_custom_fields_ibfk_1` FOREIGN KEY(id) REFERENCES music_artist (id) ON DELETE CASCADE ON UPDATE RESTRICT ) ENGINE = INNODB; EOT; + +$updates['202002071045'][] = <<<'EOT' +CREATE TABLE IF NOT EXISTS `music_review` + ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `albumId` INT(11) NOT NULL, + `aclId` INT(11) NOT NULL, + `createdBy` int(11) NOT NULL, + `modifiedBy` int(11) NOT NULL, + `rating` SMALLINT(5) UNSIGNED NOT NULL, + `title` VARCHAR(190) COLLATE utf8mb4_unicode_ci NOT NULL, + `body` TEXT collate utf8mb4_unicode_ci NOT NULL + + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +EOT; +$updates['202002071045'][] = <<<'EOT' +ALTER TABLE `music_review` + ADD KEY `aclId` (`aclId`), + ADD KEY `albumId` (`albumId`); +EOT; +$updates['202002071045'][] = <<<'EOT' +ALTER TABLE `music_review` + ADD CONSTRAINT `music_review_fk1` FOREIGN KEY (`albumId`) REFERENCES `music_album` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `music_review_fk2` FOREIGN KEY (`aclId`) REFERENCES `core_acl` (`id`); +EOT; + +//$updates['202002041445'][] = <<<'EOT' +//CREATE TABLE IF NOT EXISTS `music_album_custom_fields` +// ( id INT(11) NOT NULL PRIMARY KEY, CONSTRAINT `music_album_custom_fields_ibfk_1` FOREIGN KEY(id) +// REFERENCES music_album (id) ON DELETE CASCADE ON UPDATE RESTRICT ) ENGINE = INNODB; +//EOT; diff --git a/music/model/Album.php b/music/model/Album.php index 20f30ce..550e52e 100644 --- a/music/model/Album.php +++ b/music/model/Album.php @@ -2,7 +2,6 @@ namespace go\modules\tutorial\music\model; use go\core\orm\Property; -//use go\core\orm\CustomFieldsTrait; /** * Album model @@ -12,8 +11,7 @@ * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ class Album extends Property { -// use CustomFieldsTrait; - /** @var int */ + /** @var int */ public $id; /** @var int */ @@ -30,7 +28,7 @@ class Album extends Property { protected static function defineMapping() { return parent::defineMapping() - ->addTable("music_album", "album"); + ->addTable('music_album', 'album') + ->addScalar('reviews', 'music_review', ['id' => 'albumId']); } - } \ No newline at end of file diff --git a/music/model/Artist.php b/music/model/Artist.php index 6f3593d..f143ee1 100644 --- a/music/model/Artist.php +++ b/music/model/Artist.php @@ -5,13 +5,6 @@ use go\core\jmap\Entity; use go\core\orm\CustomFieldsTrait; -/** - * Artist model - * - * @copyright (c) 2020, Intermesh BV http://www.intermesh.nl - * @author Merijn Schering - * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 - */ class Artist extends Entity { use CustomFieldsTrait; @@ -39,13 +32,16 @@ class Artist extends Entity { /** @var array */ public $albums; + /** @var array */ + public $reviews; + /** @var int */ protected $albumcount; protected static function defineMapping() { return parent::defineMapping() - ->addTable("music_artist", "artist") - ->addArray('albums', Album::class, ['id' => 'artistId']); + ->addTable('music_artist', 'artist') + ->addMap('albums', Album::class, ['id' => 'artistId']); } /** @@ -84,5 +80,4 @@ public function getAlbumcount() :int { return count($this->albums); } - } \ No newline at end of file diff --git a/music/model/Review.php b/music/model/Review.php new file mode 100644 index 0000000..fd23d94 --- /dev/null +++ b/music/model/Review.php @@ -0,0 +1,50 @@ +addTable('music_review') + ->setQuery((new Query())->select('a.name AS albumtitle') + ->join('music_album', 'a','a.id=music_review.albumId')); + } + + protected static function defineFilters() + { + return parent::defineFilters() + ->add('albumId', function (\go\core\db\Criteria $criteria, $value, \go\core\orm\Query $query, array $filter) { + if (!empty($value)) { + $query->where(['music_review.albumId' => $value]); + } + }); + + } + + +} \ No newline at end of file diff --git a/music/views/extjs3/ArtistDetail.js b/music/views/extjs3/ArtistDetail.js index 9a2e775..7a2fe20 100644 --- a/music/views/extjs3/ArtistDetail.js +++ b/music/views/extjs3/ArtistDetail.js @@ -8,11 +8,25 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { stateId: 'music-contact-detail', // Fetch these relations for this view - relations: ["albums.genre"], + relations: ["albums.genre", "creator"], initComponent: function () { this.tbar = this.initToolbar(); + // Render a 'new review' modal + this.addReviewModal = function (v) { + var dlg = new go.modules.tutorial.music.ReviewDialog(); + dlg.setValues({albumId:v.id}); + dlg.show(); + }; + + // Render all reviews for the current album in a window (which is offered as a modal) + this.showReviewsModal = function(v) { + var dlg = new go.modules.tutorial.music.ReviewsModal(); + dlg.store.setFilter('albumId', {albumId: v.id}); + dlg.store.load(); + dlg.show(); + }; Ext.apply(this, { // all items are updated automatically if they have a "tpl" (Ext.XTemplate) property or an "onLoad" function. The panel is passed as argument. items: [ @@ -37,27 +51,63 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { }) }, - // Albums component + // Albums component, render number of reviews { collapsible: true, title: t("Albums"), xtype: "panel", - tpl: '
\ - \ + listeners: { + scope: this, + afterrender: function(box) { + box.getEl().on('click', function(e){ + + //don't execute when user selects text + if(window.getSelection().toString().length > 0) { + return; + } + + var container = box.getEl().dom.childNodes[1], + item = e.getTarget("a", box.getEl()), + i = Array.prototype.indexOf.call(container.getElementsByTagName("a"), item); + if(i >=0) { + var album = go.util.Object.convertMapToArray(this.data.albums,'id')[i]; + if(album.reviews.length > 0) { + this.showReviewsModal(album); + } else { + this.addReviewModal(album); + } + } + }, this); + } + }, + tpl: new Ext.XTemplate('
\ + \

album\ {name}\ - \ + \

\
\ -
' +
', + { + displayNumReviews: function(v){ + v = v || null; + if(v === null) { + return ""; + } else if(v.length == 0) { + return "- " + t("Write a Review")+ ""; + } else { + return "- " +v.length+" " + t("Reviews")+ "" + } + } + }) } ] }); + go.modules.tutorial.music.ArtistDetail.superclass.initComponent.call(this); this.addCustomFields(); - // this.add(new go.detail.CreateModifyPanel()); }, @@ -126,4 +176,5 @@ go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { return new Ext.Toolbar(tbarCfg); } + }); \ No newline at end of file diff --git a/music/views/extjs3/ArtistDialog.js b/music/views/extjs3/ArtistDialog.js index 88e560d..4697908 100644 --- a/music/views/extjs3/ArtistDialog.js +++ b/music/views/extjs3/ArtistDialog.js @@ -41,6 +41,7 @@ go.modules.tutorial.music.ArtistDialog = Ext.extend(go.form.Dialog, { xtype: "formgroup", name: "albums", hideLabel: true, + mapKey: 'id', // this will add dp(16) padding between rows. pad: true, @@ -52,6 +53,9 @@ go.modules.tutorial.music.ArtistDialog = Ext.extend(go.form.Dialog, { anchor: "100%" }, items: [{ + xtype: "hidden", + name: "id" + }, { xtype: "textfield", fieldLabel: t("Name"), name: "name" diff --git a/music/views/extjs3/MainPanel.js b/music/views/extjs3/MainPanel.js index 269274d..08739fb 100644 --- a/music/views/extjs3/MainPanel.js +++ b/music/views/extjs3/MainPanel.js @@ -29,7 +29,7 @@ go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { autoScroll: true, items: [ this.createGenreFilter(), - this.createFilterPanel() + this.createFilterPanel(), ] }); // Create artist detail component diff --git a/music/views/extjs3/Module.js b/music/views/extjs3/Module.js index 99ae678..dfd8781 100644 --- a/music/views/extjs3/Module.js +++ b/music/views/extjs3/Module.js @@ -13,17 +13,20 @@ go.Modules.register("tutorial", "music", { creator: {store: "User", fk: "createdBy"}, modifier: {store: "User", fk: "createdBy"}, - // 'albums' is a property of artist and has a nested relation. + // 'albums' is a property of artist and has nested relations. albums: { + type: go.Relations.TYPE_MAP, genre: {store: "Genre", fk: "genreId"} } } + }, + { + name: "Review", + relations: { + creator: {store: "User", fk:"createdBy"}, + modifier: {store: "User", fk: "modifiedBy"} + } } - // }, - // { - // name: "Album", - // relations: {store: "Genre", fk: "genreId"} - // } ], //Put code to initialize the module here after the user is authenticated diff --git a/music/views/extjs3/ReviewDialog.js b/music/views/extjs3/ReviewDialog.js new file mode 100644 index 0000000..a7a787b --- /dev/null +++ b/music/views/extjs3/ReviewDialog.js @@ -0,0 +1,61 @@ +go.modules.tutorial.music.ReviewDialog = Ext.extend(go.form.Dialog, { + stateId: 'album-review', + title: t("Review"), + entityStore: "Review", + width: dp(800), + height: dp(600), + maximizable: false, + collapsible: false, + modal: true, + + initFormItems: function () { + + this.addPanel(new go.permissions.SharePanel()); + + var items = [{ + xtype: 'fieldset', + anchor: "100% 100%", + items: [{ + xtype: 'textfield', + name: 'title', + fieldLabel: t("Title"), + anchor: '100%', + allowBlank: false + }, + { + xtype: 'radiogroup', + fieldLabel: t("Rating"), + name: "rating", + value: null, + items: [ + {boxLabel: t("It stinks"), inputValue: 1}, + {boxLabel: t("Meh"), inputValue: 2}, + {boxLabel: t("It's OK"), inputValue: 3}, + {boxLabel: t("It's pretty good"), inputValue: 4}, + {boxLabel: t("A stroke of genius"), inputValue: 5} + ] + }, + { + xtype: 'xhtmleditor', + name: 'body', + fieldLabel: "", + hideLabel: true, + anchor: '0 -90', + allowBlank: false, + listeners: { + scope: this, + ctrlenter: function() { + this.submit(); + } + } + }] + } + ]; + + return items; + }, + + onLoad : function(entityValues) { + this.supr().onLoad.call(this, entityValues); + } +}); diff --git a/music/views/extjs3/ReviewsModal.js b/music/views/extjs3/ReviewsModal.js new file mode 100644 index 0000000..ad96658 --- /dev/null +++ b/music/views/extjs3/ReviewsModal.js @@ -0,0 +1,197 @@ +go.modules.tutorial.music.ReviewsModal = Ext.extend(go.Window, { + stateId: 'album-reviews', + title: t("Reviews"), + width: dp(1000), + height: dp(800), + maximizable: true, + collapsible: false, + modal: true, + stateful: true, + layout: 'fit', + initComponent: function () { + this.tools = [{ + id: "add", + handler: function () { + var dlg = new go.modules.tutorial.music.ReviewDialog(); + dlg.setValues({albumId: this.albumid}); + dlg.show(); + } + }]; + + this.store = new go.data.Store({ + fields: [ + 'id', + 'title', + 'body', + 'rating', + 'albumtitle', + 'createdBy', + {name: 'creator', type: "relation"}, + 'albumId', 'aclId', "permissionLevel" + ], + entityStore: "Review" + }); + + // Use a Group Office store that is connected with an go.data.EntityStore for automatic updates. + this.store.on('load', function (store, records, options) { + this.updateView(); + this.updateTitle(); + this.toggleAddBtn(); + }, this); + + this.store.on('remove', function () { + this.updateView(); + this.toggleAddBtn(); + }, this); + + this.on('destroy', function () { + this.store.destroy(); + }, this); + + this.on("expand", function () { + this.updateView(); + }, this); + + // Add a simple context menu. Make sure that the correct permissions are set + this.contextMenu = new Ext.menu.Menu({ + items: [{ + iconCls: 'ic-delete', + text: t("Delete"), + handler: function () { + + Ext.MessageBox.confirm(t("Confirm delete"), t("Are you sure you want to delete this item?"), function (btn) { + if (btn !== "yes") { + return; + } + go.Db.store("Review").set({destroy: [this.contextMenu.record.id]}); + }, this); + + }, + scope: this + }, { + iconCls: 'ic-edit', + text: t("Edit"), + handler: function () { + var dlg = new go.modules.tutorial.music.ReviewDialog(); + dlg.load(this.contextMenu.record.id).show(); + }, + scope: this + }] + }); + + var cntrClass = Ext.extend(Ext.Container, { + initComponent: function () { + Ext.Container.superclass.initComponent.call(this); + Ext.applyIf(this, go.panels.ScrollLoader); + this.initScrollLoader(); + }, + store: this.store, + scrollUp: true + }); + + this.items = [ + this.commentsContainer = new cntrClass({ + region: 'center', + autoScroll: true + }) + ]; + + go.modules.tutorial.music.ReviewsModal.superclass.initComponent.call(this); + }, + + updateView: function () { + this.commentsContainer.removeAll(); + this.store.each(function (r) { + var mineCls = r.get("createdBy") == go.User.id ? 'mine' : ''; + var readMore = new go.detail.ReadMore({ + cls: mineCls + }); + var creator = r.get("creator"); + if (!creator) { + creator = { + displayName: t("Unknown user") + }; + } + var avatar = { + xtype: 'box', + autoEl: { + tag: 'span', 'ext:qtip': t('{author} wrote: ') + .replace('{author}', creator.displayName) + }, + cls: 'photo ' + mineCls + }; + if (creator.avatarId) { + avatar.style = 'background-image: url(' + go.Jmap.thumbUrl(creator.avatarId, { + w: 40, + h: 40, + zc: 1 + }) + ');background-color: transparent;'; + } else { + avatar.html = go.util.initials(creator.displayName); + avatar.style = 'background-image: none'; + } + readMore.setText(this.getReviewText(r)); + + this.commentsContainer.add({ + xtype: "container", + cls: 'go-messages', + items: [{ + xtype: 'container', + label: t("Creator"), + items: [avatar, readMore] + }] + }); + // Add a context menu, make permissions dependent on ACL + readMore.on('render', function (me) { + me.getEl().on("contextmenu", function (e, target, obj) { + e.stopEvent(); + + if (r.data.permissionLevel >= go.permissionLevels.write) { + this.contextMenu.record = r; + this.contextMenu.showAt(e.xy); + } + + }, this); + }, this); + }, this); + + this.doLayout(); + var height = 7; // padding on composer + this.commentsContainer.items.each(function (item, i) { + height += item.getOuterSize().height; + }); + }, + + // Update window title by adding the album title + updateTitle: function () { + var r = this.store.getAt(0), title = this.title; + if (typeof (r) !== "undefined") { + this.setTitle(t("Reviews")+" " + t('for') + " " + + Ext.util.Format.htmlEncode(r.get('albumtitle'))); + } else { + this.setTitle(t("Reviews")); + } + }, + + // Check whether current user had added a review. If they have, hide the add button. + toggleAddBtn: function () { + if (this.store.query('createdBy', go.User.id).getCount() > 0) { + this.tools.add.hide(); + } else { + var r = this.store.getAt(0); + if (typeof (r) !== "undefined") { + this.tools.add.albumid = r.get("albumId"); + } + } + }, + + // Render the review text in a nice fashion + getReviewText: function (r) { + var s = "

" + r.get("title") + "

"; + for (var ii = 1; ii <= 5; ii++) { + s += "star" + (r.get('rating') < ii ? "_border" : "") + ""; + } + s += "

" + Ext.util.Format.htmlDecode(r.get('body')) + "

"; + return s; + } +}); diff --git a/music/views/extjs3/scripts.txt b/music/views/extjs3/scripts.txt index 2281d25..1efd2b0 100644 --- a/music/views/extjs3/scripts.txt +++ b/music/views/extjs3/scripts.txt @@ -4,4 +4,6 @@ GenreFilter.js ArtistGrid.js GenreCombo.js ArtistDialog.js -ArtistDetail.js \ No newline at end of file +ArtistDetail.js +ReviewDialog.js +ReviewsModal.js \ No newline at end of file From ffdcbcd8a182263c54f17b38501dfc89a57ab4b2 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Mon, 17 Feb 2020 12:09:37 +0100 Subject: [PATCH 05/17] Code review: updated reviews model to auto-update Artist entity upos savir or deleting --- music/model/Review.php | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/music/model/Review.php b/music/model/Review.php index fd23d94..ec73ba8 100644 --- a/music/model/Review.php +++ b/music/model/Review.php @@ -3,6 +3,7 @@ namespace go\modules\tutorial\music\model; +use Exception; use go\core\acl\model\AclOwnerEntity; use go\core\orm\Query; @@ -35,6 +36,51 @@ protected static function defineMapping() ->join('music_album', 'a','a.id=music_review.albumId')); } + protected function internalSave() + { + if($this->isNew()) { + $this->albumtitle = go()->getDbConnection() + ->selectSingleValue('name') + ->from('music_album') + ->where(['id' => $this->albumId]) + ->single(); + } + + $this->changeArtist([$this->albumId]); + + return parent::internalSave(); + } + + + protected static function internalDelete(Query $query) + { + //Create clone to avoid changes to the original delete query object + $deleteQuery = clone $query; + + //Select albums of artists affected by this delete + $deleteQuery->selectSingleValue('albumId'); + + static::changeArtist($deleteQuery->all()); + + return parent::internalDelete($query); + } + + /** + * Our review has effect on Artist entities because they implement getAlbumCount(). + * + * @param array $albumIds + * @throws Exception + */ + private static function changeArtist(array $albumIds) { + Artist::entityType()->changes( + go()->getDbConnection() + ->select('art.id, null, 0') + ->from('music_artist', 'art') + ->join('music_album', 'alb', 'alb.artistId = art.id') + ->where('alb.id', 'IN', $albumIds) + ); + } + protected static function defineFilters() { return parent::defineFilters() From 71037b85d8220ce192b05b5a63ab48cb638c5629 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Mon, 17 Feb 2020 14:10:41 +0100 Subject: [PATCH 06/17] Feedback MS --- music/install/install.sql | 118 +++++++++++++++++++------------ music/model/Artist.php | 4 +- music/model/Review.php | 7 +- music/views/extjs3/ArtistGrid.js | 4 +- 4 files changed, 79 insertions(+), 54 deletions(-) diff --git a/music/install/install.sql b/music/install/install.sql index 82837d3..a3d1351 100644 --- a/music/install/install.sql +++ b/music/install/install.sql @@ -3,49 +3,51 @@ SET AUTOCOMMIT = 0; START TRANSACTION; SET time_zone = "+00:00"; - -- -- Table structure for table `music_album` -- -CREATE TABLE `music_album` ( - `id` int(11) NOT NULL, +CREATE TABLE IF NOT EXISTS `music_album` ( + `id` int(11) NOT NULL AUTO_INCREMENT, `artistId` int(11) NOT NULL, `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, `releaseDate` date NOT NULL, - `genreId` int(11) NOT NULL + `genreId` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `artistId` (`artistId`), + KEY `genreId` (`genreId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; --- --- Dumping data for table `music_album` --- - -INSERT INTO `music_album` (`id`, `artistId`, `name`, `releaseDate`, `genreId`) VALUES -(1, 3, 'The Doors', '1967-01-04', 2), -(1, 3, 'Strange Days', '1967-09-25', 2); - -- -------------------------------------------------------- -- -- Table structure for table `music_artist` -- -CREATE TABLE `music_artist` ( - `id` int(11) NOT NULL, +CREATE TABLE IF NOT EXISTS `music_artist` ( + `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, `photo` binary(40) DEFAULT NULL, `createdAt` datetime NOT NULL, `modifiedAt` datetime NOT NULL, `createdBy` int(11) NOT NULL, - `modifiedBy` int(11) NOT NULL + `modifiedBy` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `photo` (`photo`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- -------------------------------------------------------- + -- --- Dumping data for table `music_artist` +-- Table structure for table `music_artist_custom_fields` -- -INSERT INTO `music_artist` (`id`, `name`, `photo`, `createdAt`, `modifiedAt`, `createdBy`, `modifiedBy`) VALUES -(1, 'The Doors', NULL, '2020-02-03 13:05:25', '2020-02-03 13:05:25', 1, 1); +CREATE TABLE IF NOT EXISTS `music_artist_custom_fields` ( + `id` int(11) NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT 1, + `bio` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -53,60 +55,82 @@ INSERT INTO `music_artist` (`id`, `name`, `photo`, `createdAt`, `modifiedAt`, `c -- Table structure for table `music_genre` -- -CREATE TABLE `music_genre` ( - `id` int(11) NOT NULL, - `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL +CREATE TABLE IF NOT EXISTS `music_genre` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- -------------------------------------------------------- + -- --- Dumping data for table `music_genre` +-- Table structure for table `music_review` -- -INSERT INTO `music_genre` (`id`, `name`) VALUES -(1, 'Pop'), -(2, 'Rock'), -(3, 'Blues'), -(4, 'Jazz'); +CREATE TABLE IF NOT EXISTS `music_review` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `albumId` int(11) NOT NULL, + `aclId` int(11) NOT NULL, + `createdBy` int(11) NOT NULL, + `modifiedBy` int(11) NOT NULL, + `rating` smallint(5) UNSIGNED NOT NULL, + `title` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, + `body` text COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + KEY `aclId` (`aclId`), + KEY `albumId` (`albumId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- --- Indexes for dumped tables +-- Constraints for dumped tables -- -- --- Indexes for table `music_album` +-- Constraints for table `music_album` -- ALTER TABLE `music_album` - ADD PRIMARY KEY (`id`), - ADD KEY `artistId` (`artistId`), - ADD KEY `genreId` (`genreId`); + ADD CONSTRAINT `music_album_ibfk_1` FOREIGN KEY (`artistId`) REFERENCES `music_artist` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `music_album_ibfk_2` FOREIGN KEY (`genreId`) REFERENCES `music_genre` (`id`); -- --- Indexes for table `music_artist` +-- Constraints for table `music_artist` -- ALTER TABLE `music_artist` - ADD PRIMARY KEY (`id`), - ADD KEY `photo` (`photo`); + ADD CONSTRAINT `music_artist_ibfk_1` FOREIGN KEY (`photo`) REFERENCES `core_blob` (`id`); -- --- Indexes for table `music_genre` +-- Constraints for table `music_artist_custom_fields` -- -ALTER TABLE `music_genre` - ADD PRIMARY KEY (`id`); +ALTER TABLE `music_artist_custom_fields` + ADD CONSTRAINT `music_artist_custom_fields_ibfk_1` FOREIGN KEY (`id`) REFERENCES `music_artist` (`id`) ON DELETE CASCADE; -- --- Constraints for dumped tables +-- Constraints for table `music_review` -- +ALTER TABLE `music_review` + ADD CONSTRAINT `music_review_fk1` FOREIGN KEY (`albumId`) REFERENCES `music_album` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `music_review_fk2` FOREIGN KEY (`aclId`) REFERENCES `core_acl` (`id`); +COMMIT; -- --- Constraints for table `music_album` +-- Dumping data for table `music_artist` -- -ALTER TABLE `music_album` - ADD CONSTRAINT `music_album_ibfk_1` FOREIGN KEY (`artistId`) REFERENCES `music_artist` (`id`) ON DELETE CASCADE, - ADD CONSTRAINT `music_album_ibfk_2` FOREIGN KEY (`genreId`) REFERENCES `music_genre` (`id`); -- --- Constraints for table `music_artist` +-- Dumping data for table `music_genre` -- -ALTER TABLE `music_artist` - ADD CONSTRAINT `music_artist_ibfk_1` FOREIGN KEY (`photo`) REFERENCES `core_blob` (`id`); -COMMIT; + +INSERT INTO `music_genre` (`id`, `name`) VALUES +(1, 'Pop'), +(2, 'Rock'), +(3, 'Blues'), +(4, 'Jazz'); + + +-- +-- Dumping data for table `music_album` +-- + +INSERT INTO `music_album` (`id`, `artistId`, `name`, `releaseDate`, `genreId`) VALUES +(1, 3, 'The Doors', '1967-01-04', 2), +(1, 3, 'Strange Days', '1967-09-25', 2); diff --git a/music/model/Artist.php b/music/model/Artist.php index f143ee1..70fa5f3 100644 --- a/music/model/Artist.php +++ b/music/model/Artist.php @@ -36,7 +36,7 @@ class Artist extends Entity { public $reviews; /** @var int */ - protected $albumcount; + protected $albumCount; protected static function defineMapping() { return parent::defineMapping() @@ -76,7 +76,7 @@ protected static function defineFilters() * * @return int */ - public function getAlbumcount() :int + public function getAlbumCount() :int { return count($this->albums); } diff --git a/music/model/Review.php b/music/model/Review.php index ec73ba8..97583c6 100644 --- a/music/model/Review.php +++ b/music/model/Review.php @@ -33,12 +33,12 @@ protected static function defineMapping() return parent::defineMapping() ->addTable('music_review') ->setQuery((new Query())->select('a.name AS albumtitle') - ->join('music_album', 'a','a.id=music_review.albumId')); + ->join('music_album', 'a', 'a.id=music_review.albumId')); } protected function internalSave() { - if($this->isNew()) { + if ($this->isNew()) { $this->albumtitle = go()->getDbConnection() ->selectSingleValue('name') ->from('music_album') @@ -71,7 +71,8 @@ protected static function internalDelete(Query $query) * @param array $albumIds * @throws Exception */ - private static function changeArtist(array $albumIds) { + private static function changeArtist(array $albumIds) + { Artist::entityType()->changes( go()->getDbConnection() ->select('art.id, null, 0') diff --git a/music/views/extjs3/ArtistGrid.js b/music/views/extjs3/ArtistGrid.js index 439ffd3..d18acb5 100644 --- a/music/views/extjs3/ArtistGrid.js +++ b/music/views/extjs3/ArtistGrid.js @@ -19,7 +19,7 @@ go.modules.tutorial.music.ArtistGrid = Ext.extend(go.grid.GridPanel, { // Every entity has permission levels. go.permissionLevels.read, write, // writeAndDelete and manage 'permissionLevel', - 'albumcount' + 'albumCount' ], // The connected entity store. When Artists are changed the store will @@ -59,7 +59,7 @@ go.modules.tutorial.music.ArtistGrid = Ext.extend(go.grid.GridPanel, { id: 'albumcount', sortable: false, header: t('album_count','music','tutorial'), - dataIndex: 'albumcount', + dataIndex: 'albumCount', width: dp(80) }, { From e25a9eea77c5b431a64bb444bdd60f9249064b53 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Fri, 5 Jul 2024 09:39:18 +0200 Subject: [PATCH 07/17] Update README --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5d1430..d6d1d0e 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,9 @@ Clone this as "tutorial" inside the go/modules folder: ``` cd go/modules git clone https://github.com/Intermesh/groupoffice-tutorial.git tutorial -```` \ No newline at end of file +```` + +## TODO + +- Update compatibility to >6.4 +- Refactor into GOUI From 678381ebb2fcc107b81569593f23d6b3c08b0c23 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Fri, 22 Nov 2024 10:17:44 +0100 Subject: [PATCH 08/17] Initial commit --- README.md | 4 +- music/Module.php | 24 +- music/controller/Artist.php | 54 +- music/controller/Genre.php | 57 +- music/install/install.sql | 30 +- music/install/uninstall.sql | 6 +- music/model/Album.php | 10 +- music/model/Artist.php | 56 +- music/model/Genre.php | 24 +- music/views/extjs3/ArtistDetail.js | 129 -- music/views/extjs3/ArtistDialog.js | 75 -- music/views/extjs3/ArtistGrid.js | 110 -- music/views/extjs3/GenreCombo.js | 22 - music/views/extjs3/GenreFilter.js | 136 -- music/views/extjs3/MainPanel.js | 201 --- music/views/extjs3/Module.js | 27 - music/views/extjs3/scripts.txt | 7 - music/views/extjs3/themes/default/style.css | 0 music/views/goui/package-lock.json | 1349 +++++++++++++++++++ music/views/goui/package.json | 19 + music/views/goui/script/Artist.ts | 19 + music/views/goui/script/ArtistDetail.ts | 160 +++ music/views/goui/script/ArtistTable.ts | 64 + music/views/goui/script/ArtistWindow.ts | 47 + music/views/goui/script/GenreTable.ts | 39 + music/views/goui/script/Index.ts | 35 + music/views/goui/script/Main.ts | 201 +++ music/views/goui/style/style.scss | 11 + music/views/goui/tsconfig.json | 7 + 29 files changed, 2109 insertions(+), 814 deletions(-) delete mode 100644 music/views/extjs3/ArtistDetail.js delete mode 100644 music/views/extjs3/ArtistDialog.js delete mode 100644 music/views/extjs3/ArtistGrid.js delete mode 100644 music/views/extjs3/GenreCombo.js delete mode 100644 music/views/extjs3/GenreFilter.js delete mode 100644 music/views/extjs3/MainPanel.js delete mode 100644 music/views/extjs3/Module.js delete mode 100644 music/views/extjs3/scripts.txt delete mode 100644 music/views/extjs3/themes/default/style.css create mode 100644 music/views/goui/package-lock.json create mode 100644 music/views/goui/package.json create mode 100644 music/views/goui/script/Artist.ts create mode 100644 music/views/goui/script/ArtistDetail.ts create mode 100644 music/views/goui/script/ArtistTable.ts create mode 100644 music/views/goui/script/ArtistWindow.ts create mode 100644 music/views/goui/script/GenreTable.ts create mode 100644 music/views/goui/script/Index.ts create mode 100644 music/views/goui/script/Main.ts create mode 100644 music/views/goui/style/style.scss create mode 100644 music/views/goui/tsconfig.json diff --git a/README.md b/README.md index ee8c4aa..cdd7bab 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ This code belongs to the Group-Office developer tutorial: https://groupoffice.readthedocs.io/en/latest/developer/index.html -Clone this as "tutorial" insde the go/modules folder: +Clone this as "tutorial" inside the `go/modules` folder: ``` cd go/modules -clone https://github.com/Intermesh/groupoffice-tutorial.git tutorial +git clone https://github.com/Intermesh/groupoffice-tutorial.git tutorial ```` \ No newline at end of file diff --git a/music/Module.php b/music/Module.php index c46f1c7..3f3a1d6 100644 --- a/music/Module.php +++ b/music/Module.php @@ -1,17 +1,25 @@ + * @author Joachim van de Haterd * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ -class Module extends core\Module { - - public function getAuthor() { +class Module extends core\Module +{ + + public function getAuthor(): string + { return "Intermesh BV "; } - + + public function getStatus(): string + { + return self::STATUS_STABLE; + } } \ No newline at end of file diff --git a/music/controller/Artist.php b/music/controller/Artist.php index 4fe5ca1..9672de4 100644 --- a/music/controller/Artist.php +++ b/music/controller/Artist.php @@ -1,65 +1,79 @@ + * @author Joachim van de Haterd * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 - */ -class Artist extends EntityController { - + */ + +final class Artist extends EntityController +{ + /** * The class name of the entity this controller is for. - * + * * @return string */ - protected function entityClass() { + protected function entityClass(): string + { return model\Artist::class; - } - + } + /** * Handles the Artist entity's Artist/query command - * + * * @param array $params + * @throws InvalidArguments * @see https://jmap.io/spec-core.html#/query */ - public function query($params) { + public function query(array $params): ArrayObject + { return $this->defaultQuery($params); } - + /** * Handles the Artist entity's Artist/get command - * + * * @param array $params + * @return ArrayObject + * @throws \Exception * @see https://jmap.io/spec-core.html#/get */ - public function get($params) { + public function get(array $params): ArrayObject + { return $this->defaultGet($params); } - + /** * Handles the Artist entity's Artist/set command - * + * * @see https://jmap.io/spec-core.html#/set * @param array $params */ - public function set($params) { + public function set(array $params): ArrayObject + { return $this->defaultSet($params); } - - + + /** * Handles the Artist entity's Artist/changes command - * + * * @param array $params * @see https://jmap.io/spec-core.html#/changes */ - public function changes($params) { + public function changes(array $params): ArrayObject + { return $this->defaultChanges($params); } } diff --git a/music/controller/Genre.php b/music/controller/Genre.php index 552d83b..0976ae2 100644 --- a/music/controller/Genre.php +++ b/music/controller/Genre.php @@ -1,65 +1,82 @@ + * @author Joachim van de Haterd * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 - */ -class Genre extends EntityController { - + */ +final class Genre extends EntityController +{ + /** * The class name of the entity this controller is for. - * + * * @return string */ - protected function entityClass() { + protected function entityClass(): string + { return model\Genre::class; - } - + } + /** * Handles the Genre entity's Genre/query command - * + * * @param array $params + * @return ArrayObject + * @throws InvalidArguments * @see https://jmap.io/spec-core.html#/query */ - public function query($params) { + public function query(array $params): ArrayObject + { return $this->defaultQuery($params); } - + /** * Handles the Genre entity's Genre/get command - * + * * @param array $params + * @return ArrayObject + * @throws \Exception * @see https://jmap.io/spec-core.html#/get */ - public function get($params) { + public function get(array $params): ArrayObject + { return $this->defaultGet($params); } - + /** * Handles the Genre entity's Genre/set command - * + * * @see https://jmap.io/spec-core.html#/set + * @return ArrayObject * @param array $params */ - public function set($params) { + public function set(array $params): ArrayObject + { return $this->defaultSet($params); } - - + + /** * Handles the Genre entity's Genre/changes command - * + * * @param array $params + * @return ArrayObject + * @throws InvalidArguments * @see https://jmap.io/spec-core.html#/changes */ - public function changes($params) { + public function changes(array $params): ArrayObject + { return $this->defaultChanges($params); } } diff --git a/music/install/install.sql b/music/install/install.sql index 57ed277..3ba489c 100644 --- a/music/install/install.sql +++ b/music/install/install.sql @@ -1,4 +1,4 @@ -CREATE TABLE `music_album` ( +CREATE TABLE `tutorial_music_album` ( `id` int(11) NOT NULL, `artistId` int(11) NOT NULL, `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, @@ -6,7 +6,7 @@ CREATE TABLE `music_album` ( `genreId` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE `music_artist` ( +CREATE TABLE `tutorial_music_artist` ( `id` int(11) NOT NULL, `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, `photo` binary(40) DEFAULT NULL, @@ -17,44 +17,44 @@ CREATE TABLE `music_artist` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE `music_genre` ( +CREATE TABLE `tutorial_music_genre` ( `id` int(11) NOT NULL, `name` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -INSERT INTO `music_genre` (`id`, `name`) VALUES +INSERT INTO `tutorial_music_genre` (`id`, `name`) VALUES (1, 'Pop'), (2, 'Rock'), (3, 'Blues'), (4, 'Jazz'); -ALTER TABLE `music_album` +ALTER TABLE `tutorial_music_album` ADD PRIMARY KEY (`id`), ADD KEY `artistId` (`artistId`), ADD KEY `genreId` (`genreId`); -ALTER TABLE `music_artist` +ALTER TABLE `tutorial_music_artist` ADD PRIMARY KEY (`id`), ADD KEY `photo` (`photo`); -ALTER TABLE `music_genre` +ALTER TABLE `tutorial_music_genre` ADD PRIMARY KEY (`id`); -ALTER TABLE `music_album` +ALTER TABLE `tutorial_music_album` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=14; -ALTER TABLE `music_artist` +ALTER TABLE `tutorial_music_artist` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; -ALTER TABLE `music_genre` +ALTER TABLE `tutorial_music_genre` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5; -ALTER TABLE `music_album` - ADD CONSTRAINT `music_album_ibfk_1` FOREIGN KEY (`artistId`) REFERENCES `music_artist` (`id`) ON DELETE CASCADE, - ADD CONSTRAINT `music_album_ibfk_2` FOREIGN KEY (`genreId`) REFERENCES `music_genre` (`id`); +ALTER TABLE `tutorial_music_album` + ADD CONSTRAINT `music_album_ibfk_1` FOREIGN KEY (`artistId`) REFERENCES `tutorial_music_artist` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `music_album_ibfk_2` FOREIGN KEY (`genreId`) REFERENCES `tutorial_music_genre` (`id`); -ALTER TABLE `music_artist` - ADD CONSTRAINT `music_artist_ibfk_1` FOREIGN KEY (`photo`) REFERENCES `core_blob` (`id`); \ No newline at end of file +ALTER TABLE `tutorial_music_artist` + ADD CONSTRAINT `tutorial_music_artist_ibfk_1` FOREIGN KEY (`photo`) REFERENCES `core_blob` (`id`); \ No newline at end of file diff --git a/music/install/uninstall.sql b/music/install/uninstall.sql index 70189a0..e87df58 100644 --- a/music/install/uninstall.sql +++ b/music/install/uninstall.sql @@ -1,3 +1,3 @@ -DROP TABLE IF EXISTS music_album; -DROP TABLE IF EXISTS music_artist; -DROP TABLE IF EXISTS music_genre; \ No newline at end of file +DROP TABLE IF EXISTS tutorial_music_album; +DROP TABLE IF EXISTS tutorial_music_artist; +DROP TABLE IF EXISTS tutorial_music_genre; \ No newline at end of file diff --git a/music/model/Album.php b/music/model/Album.php index f329dcd..1bee5dc 100644 --- a/music/model/Album.php +++ b/music/model/Album.php @@ -1,17 +1,19 @@ + * @author Joachim van de Haterd * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ -class Album extends Property { +final class Album extends Property { /** * @@ -43,9 +45,9 @@ class Album extends Property { */ public $genreId; - protected static function defineMapping() { + protected static function defineMapping(): Mapping { return parent::defineMapping() - ->addTable("music_album", "album"); + ->addTable("tutorial_music_album", "album"); } } \ No newline at end of file diff --git a/music/model/Artist.php b/music/model/Artist.php index afa733c..8328135 100644 --- a/music/model/Artist.php +++ b/music/model/Artist.php @@ -4,55 +4,58 @@ use go\core\jmap\Entity; use go\core\orm\Filters; +use go\core\orm\Mapping; use go\core\util\DateTime; /** * Artist model * - * @copyright (c) 2019, Intermesh BV http://www.intermesh.nl + * @copyright (c) 2019-2024, Intermesh BV http://www.intermesh.nl * @author Merijn Schering + * @author Joachim van de Haterd * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ -class Artist extends Entity { +final class Artist extends Entity +{ /** - * + * * @var int */ public $id; /** - * + * * @var string */ public $name; /** - * + * * @var string */ public $photo; /** - * + * * @var DateTime */ public $createdAt; /** - * + * * @var DateTime */ public $modifiedAt; /** - * + * * @var int */ public $createdBy; /** - * + * * @var int */ public $modifiedBy; @@ -64,18 +67,20 @@ class Artist extends Entity { */ public $albums; - protected static function defineMapping() { + protected static function defineMapping(): Mapping + { return parent::defineMapping() - ->addTable("music_artist", "artist") - ->addArray('albums', Album::class, ['id' => 'artistId']); + ->addTable("tutorial_music_artist", "artist") + ->addArray('albums', Album::class, ['id' => 'artistId']); } /** - * This function returns the columns to search when using the "text" filter. - */ - public static function textFilterColumns() { - return ['name']; - } + * This function returns the columns to search when using the "text" filter. + */ + public static function textFilterColumns(): array + { + return ['name']; + } /** * Defines JMAP filters @@ -86,15 +91,16 @@ public static function textFilterColumns() { * * @return Filters */ - protected static function defineFilters() { + protected static function defineFilters(): Filters + { return parent::defineFilters() - ->add('genres', function (\go\core\db\Criteria $criteria, $value, \go\core\orm\Query $query, array $filter) { - if (!empty($value)) { - $query->join('music_album', 'album', 'album.artistId = artist.id') - ->groupBy(['artist.id']) // group the results by id to filter out duplicates because of the join - ->where(['album.genreId' => $value]); - } - }); + ->add('genres', function (\go\core\db\Criteria $criteria, $value, \go\core\orm\Query $query, array $filter) { + if (!empty($value)) { + $query->join('tutorial_music_album', 'album', 'album.artistId = artist.id') + ->groupBy(['artist.id']) // group the results by id to filter out duplicates because of the join + ->where(['album.genreId' => $value]); + } + }); } } diff --git a/music/model/Genre.php b/music/model/Genre.php index 4226ec4..4e712df 100644 --- a/music/model/Genre.php +++ b/music/model/Genre.php @@ -1,33 +1,37 @@ + * @author Joachim van de Haterd * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ +final class Genre extends Entity +{ -class Genre extends Entity { - /** - * + * * @var int - */ + */ public $id; /** - * + * * @var string - */ + */ public $name; - protected static function defineMapping() { + protected static function defineMapping(): Mapping + { return parent::defineMapping() - ->addTable("music_genre", "genre"); + ->addTable("tutorial_music_genre", "genre"); } } \ No newline at end of file diff --git a/music/views/extjs3/ArtistDetail.js b/music/views/extjs3/ArtistDetail.js deleted file mode 100644 index fea634d..0000000 --- a/music/views/extjs3/ArtistDetail.js +++ /dev/null @@ -1,129 +0,0 @@ -go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { - - // The entity store is connected. The detail view is automatically updated. - entityStore: "Artist", - - //set to true to enable state saving - stateful: false, - stateId: 'music-contact-detail', - - // Fetch these relations for this view - relations: ["albums.genre"], - - initComponent: function () { - this.tbar = this.initToolbar(); - - Ext.apply(this, { - // all items are updated automatically if they have a "tpl" (Ext.XTemplate) property or an "onLoad" function. The panel is passed as argument. - items: [ - - //Artist name component - { - cls: 'content', - xtype: 'box', - tpl: '

{name}

' - }, - - //Render the avatar - { - xtype: "box", - cls: "content", - tpl: new Ext.XTemplate('
\ -
', - { - getCls: function (isOrganization) { - return isOrganization ? "organization" : ""; - }, - getStyle: function (photoBlobId) { - return photoBlobId ? 'background-image: url(' + go.Jmap.downloadUrl(photoBlobId) + ')"' : ""; - } - }) - }, - - // Albums component - { - collapsible: true, - title: t("Albums"), - xtype: "panel", - tpl: '
\ - \ -

album\ - {name}\ - \ -

\ -
\ -
' - } - ] - }); - - go.modules.tutorial.music.ArtistDetail.superclass.initComponent.call(this); - - }, - - onLoad: function () { - - // Enable edit button according to permission level. - this.getTopToolbar().getComponent("edit").setDisabled(this.data.permissionLevel < go.permissionLevels.write); - this.deleteItem.setDisabled(this.data.permissionLevel < go.permissionLevels.writeAndDelete); - - go.modules.tutorial.music.ArtistDetail.superclass.onLoad.call(this); - }, - - initToolbar: function () { - - var items = this.tbar || []; - - items = items.concat([ - '->', - { - itemId: "edit", - iconCls: 'ic-edit', - tooltip: t("Edit"), - handler: function (btn, e) { - var dlg = new go.modules.tutorial.music.ArtistDialog(); - dlg.show(); - dlg.load(this.data.id); - }, - scope: this - }, - - { - - iconCls: 'ic-more-vert', - menu: [ - { - iconCls: "ic-print", - text: t("Print"), - handler: function () { - this.body.print({ title: this.data.name }); - }, - scope: this - }, - '-', - this.deleteItem = new Ext.menu.Item({ - itemId: "delete", - iconCls: 'ic-delete', - text: t("Delete"), - handler: function () { - Ext.MessageBox.confirm(t("Confirm delete"), t("Are you sure you want to delete this item?"), function (btn) { - if (btn != "yes") { - return; - } - this.entityStore.set({ destroy: [this.currentId] }); - }, this); - }, - scope: this - }) - - ] - }]); - - var tbarCfg = { - disabled: true, - items: items - }; - - return new Ext.Toolbar(tbarCfg); - } -}); diff --git a/music/views/extjs3/ArtistDialog.js b/music/views/extjs3/ArtistDialog.js deleted file mode 100644 index 2f664bf..0000000 --- a/music/views/extjs3/ArtistDialog.js +++ /dev/null @@ -1,75 +0,0 @@ -go.modules.tutorial.music.ArtistDialog = Ext.extend(go.form.Dialog, { - // Change to true to remember state - stateful: false, - stateId: 'music-aritst-dialog', - title: t('Artist'), - - //The dialog set's entities in an go.data.EntityStore. This store notifies all - //connected go.data.Store view stores to update. - entityStore: "Artist", - autoHeight: true, - - // return an array of form items here. - initFormItems: function () { - return [{ - // it's recommended to wrap all fields in field sets for consistent style. - xtype: 'fieldset', - title: t("Artist information"), - items: [ - this.avatarComp = new go.form.ImageField({ - name: 'photo' - }), - - { - xtype: 'textfield', - name: 'name', - fieldLabel: t("Name"), - anchor: '100%', - allowBlank: false - }] - }, - - { - xtype: "fieldset", - title: t("Albums"), - - items: [ - { - //For relational properties we can use the "go.form.FormGroup" component. - //It's a sub form for the "albums" array property. - - xtype: "formgroup", - name: "albums", - hideLabel: true, - - // this will add dp(16) padding between rows. - pad: true, - - //the itemCfg is used to create a component for each "album" in the array. - itemCfg: { - defaults: { - anchor: "100%" - }, - items: [{ - xtype: "textfield", - fieldLabel: t("Name"), - name: "name" - }, - - { - xtype: "datefield", - fieldLabel: t("Release date"), - name: "releaseDate" - }, - - { - xtype: "genrecombo" - } - ] - } - } - ] - } - ]; - } -}); diff --git a/music/views/extjs3/ArtistGrid.js b/music/views/extjs3/ArtistGrid.js deleted file mode 100644 index 6a8b960..0000000 --- a/music/views/extjs3/ArtistGrid.js +++ /dev/null @@ -1,110 +0,0 @@ -go.modules.tutorial.music.ArtistGrid = Ext.extend(go.grid.GridPanel, { - initComponent: function () { - - // Use a Group Office store that is connected with an go.data.EntityStore for automatic updates. - this.store = new go.data.Store({ - fields: [ - 'id', - 'name', - 'photo', //This is a blob id. A download URL can be retreived with go.Jmap.downloadUrl(record.data.photo) - - {name: 'createdAt', type: 'date'}, - {name: 'modifiedAt', type: 'date'}, - - // You can use "relation" as a store data type. This will autmatically - // fetch the related entity by the definition in Module.js. - {name: 'creator', type: "relation"}, - {name: 'modifier', type: "relation"}, - - // Every entity has permission levels. go.permissionLevels.read, write, - // writeAndDelete and manage - 'permissionLevel' - ], - - // The connected entity store. When Artists are changed the store will - // update automatically - entityStore: "Artist" - }); - - Ext.apply(this, { - - columns: [ - { - id: 'id', - hidden: true, - header: 'ID', - width: dp(40), - sortable: true, - dataIndex: 'id' - }, - { - id: 'name', - header: t('Name'), - width: dp(75), - sortable: true, - dataIndex: 'name', - renderer: function (value, metaData, record, rowIndex, colIndex, store) { - - //Render an avatar for the artist. - var style = record.data.photo ? 'background-image: url(' + go.Jmap.downloadUrl(record.data.photo) + ')"' : ''; - - return '
\ -
\ -
' + record.get('name') + '
\ -
'; - } - }, - { - xtype: "datecolumn", - id: 'createdAt', - header: t('Created at'), - width: dp(160), - sortable: true, - dataIndex: 'createdAt', - hidden: true - }, - { - xtype: "datecolumn", - hidden: false, - id: 'modifiedAt', - header: t('Modified at'), - width: dp(160), - sortable: true, - dataIndex: 'modifiedAt' - }, - { - hidden: true, - header: t('Created by'), - width: dp(160), - sortable: true, - dataIndex: 'creator', - renderer: function (v) { - return v ? v.displayName : "-"; - } - }, - { - hidden: true, - header: t('Modified by'), - width: dp(160), - sortable: true, - dataIndex: 'modifier', - renderer: function (v) { - return v ? v.displayName : "-"; - } - } - ], - - viewConfig: { - emptyText: 'description

' + t("No items to display") + '

' - }, - - autoExpandColumn: 'name', - - // Change to true to remember grid state - stateful: false, - stateId: 'music-artist-grid' - }); - - go.modules.tutorial.music.ArtistGrid.superclass.initComponent.call(this); - } -}); diff --git a/music/views/extjs3/GenreCombo.js b/music/views/extjs3/GenreCombo.js deleted file mode 100644 index b37e761..0000000 --- a/music/views/extjs3/GenreCombo.js +++ /dev/null @@ -1,22 +0,0 @@ -go.modules.tutorial.music.GenreCombo = Ext.extend(go.form.ComboBox, { - fieldLabel: t("Genre"), - hiddenName: 'genreId', - anchor: '100%', - emptyText: t("Please select..."), - pageSize: 50, - valueField: 'id', - displayField: 'name', - triggerAction: 'all', - editable: true, - selectOnFocus: true, - forceSelection: true, - allowBlank: false, - store: { - xtype: "gostore", - fields: ['id', 'name'], - entityStore: "Genre" - } -}); - -// Register an xtype so we can use the component easily. -Ext.reg("genrecombo", go.modules.tutorial.music.GenreCombo); diff --git a/music/views/extjs3/GenreFilter.js b/music/views/extjs3/GenreFilter.js deleted file mode 100644 index 33342dc..0000000 --- a/music/views/extjs3/GenreFilter.js +++ /dev/null @@ -1,136 +0,0 @@ -go.modules.tutorial.music.GenreFilter = Ext.extend(go.grid.GridPanel, { - viewConfig: { - forceFit: true, - autoFill: true - }, - - //This component is going to be the side navigation - cls: 'go-sidenav', - - hideHeaders: true, - - initComponent: function () { - - // Row actions is a special grid column with an actions menu in it. - var actions = this.initRowActions(); - - // A selection model with checkboxes in this filter. - var selModel = new Ext.grid.CheckboxSelectionModel(); - - // A toolbar that consists out of two rows. - var tbar = { - xtype: "container", - items: [ - { - items: this.tbar || [], - xtype: 'toolbar' - }, - new Ext.Toolbar({ - items: [{xtype: "selectallcheckbox"}] - }) - ] - }; - - Ext.apply(this, { - - tbar: tbar, - - // We use a "go.data.Store" that connects with an Entity store. This store updates automatically when entities change. - store: new go.data.Store({ - fields: ['id', 'name', 'aclId', "permissionLevel"], - entityStore: "Genre" - }), - selModel: selModel, - plugins: [actions], - columns: [ - // The checkbox selection model must be added as a column too - selModel, - { - id: 'name', - header: t('Name'), - sortable: false, - dataIndex: 'name', - hideable: false, - draggable: false, - menuDisabled: true - }, - // The actions column showing a menu with delete and edit items. - actions - ], - - // Change to true to remember the state of the panel - stateful: false, - stateId: 'music-genre-filter' - }); - - go.modules.tutorial.music.GenreFilter.superclass.initComponent.call(this); - }, - - initRowActions: function () { - - var actions = new Ext.ux.grid.RowActions({ - menuDisabled: true, - hideable: false, - draggable: false, - fixed: true, - header: '', - hideMode: 'display', - keepSelection: true, - - actions: [{ - iconCls: 'ic-more-vert' - }] - }); - - actions.on({ - action: function (grid, record, action, row, col, e, target) { - this.showMoreMenu(record, e); - }, - scope: this - }); - - return actions; - - }, - - showMoreMenu: function (record, e) { - if (!this.moreMenu) { - this.moreMenu = new Ext.menu.Menu({ - items: [ - { - itemId: "edit", - - // We use Material design icons. Look them up at https://material.io/tools/icons/?style=baseline. You can use ic-{name} as class names. - iconCls: 'ic-edit', - text: t("Edit"), - handler: function () { - var dlg = new go.modules.tutorial.music.GenreForm(); - dlg.load(this.moreMenu.record.id).show(); - }, - scope: this - }, { - itemId: "delete", - iconCls: 'ic-delete', - text: t("Delete"), - handler: function () { - Ext.MessageBox.confirm(t("Confirm delete"), t("Are you sure you want to delete this item?"), function (btn) { - if (btn != "yes") { - return; - } - go.Db.store("Genre").set({destroy: [this.moreMenu.record.id]}); - }, this); - }, - scope: this - } - ] - }) - } - - this.moreMenu.getComponent("edit").setDisabled(record.get("permissionLevel") < go.permissionLevels.manage); - this.moreMenu.getComponent("delete").setDisabled(record.get("permissionLevel") < go.permissionLevels.manage); - - this.moreMenu.record = record; - - this.moreMenu.showAt(e.getXY()); - } -}); diff --git a/music/views/extjs3/MainPanel.js b/music/views/extjs3/MainPanel.js deleted file mode 100644 index 56550e9..0000000 --- a/music/views/extjs3/MainPanel.js +++ /dev/null @@ -1,201 +0,0 @@ -go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { - - // Use a responsive layout - layout: "responsive", - - // change responsive mode on 1000 pixels - layoutConfig: { - triggerWidth: 1000 - }, - - initComponent: function () { - - //create the genre filter component - this.genreFilter = new go.modules.tutorial.music.GenreFilter({ - region: "west", - width: dp(300), - - //render a split bar for resizing - split: true, - - tbar: [{ - xtype: "tbtitle", - text: t("Genres") - }, - '->', - - //add back button for smaller screens - { - //this class will hide it on larger screens - cls: 'go-narrow', - iconCls: "ic-arrow-forward", - tooltip: t("Artists"), - handler: function () { - this.artistGrid.show(); - }, - scope: this - } - ] - }); - - //Create the artist grid - this.artistGrid = new go.modules.tutorial.music.ArtistGrid({ - region: "center", - - tbar: [ - //add a hamburger button for smaller screens - { - //this class will hide the button on large screens - cls: 'go-narrow', - iconCls: "ic-menu", - handler: function () { - this.genreFilter.show(); - }, - scope: this - }, - '->', - { - xtype: 'tbsearch' - }, - - // add button for creating new artists - this.addButton = new Ext.Button({ - iconCls: 'ic-add', - tooltip: t('Add'), - handler: function (btn) { - var dlg = new go.modules.tutorial.music.ArtistDialog({ - formValues: { - // you can pass form values like this - } - }); - dlg.show(); - }, - scope: this - }), - { - iconCls: 'ic-more-vert', - menu: [ - { - itemId: "delete", - iconCls: 'ic-delete', - text: t("Delete"), - handler: function () { - this.artistGrid.deleteSelected(); - }, - scope: this - } - ] - } - ], - - listeners: { - rowdblclick: this.onGridDblClick, - keypress: this.onGridKeyPress, - scope: this - } - }); - - // Every entity automatically configures a route. Route to the entity when selecting it in the grid. - this.artistGrid.on('navigate', function (grid, rowIndex, record) { - go.Router.goto("artist/" + record.id); - }, this); - - // Create artist detail component - this.artistDetail = new go.modules.tutorial.music.ArtistDetail({ - region: "center", - tbar: [ - //add a back button for small screens - { - // this class will hide the button on large screens - cls: 'go-narrow', - iconCls: "ic-arrow-back", - handler: function () { - go.Router.goto("music"); - }, - scope: this - }] - }); - - //Wrap the grids into another panel with responsive layout for the 3 column responsive layout to work. - this.westPanel = new Ext.Panel({ - region: "west", - layout: "responsive", - stateId: "go-music-west", - split: true, - width: dp(800), - narrowWidth: dp(500), //this will only work for panels inside another panel with layout=responsive. Not ideal but at the moment the only way I could make it work - items: [ - this.artistGrid, //first item is shown as default in narrow mode. - this.genreFilter - ] - }); - - //add the components to the main panel's items. - this.items = [ - this.westPanel, //first is default in narrow mode - this.artistDetail - ]; - - // Call the parent class' initComponent - go.modules.tutorial.music.MainPanel.superclass.initComponent.call(this); - - //Attach lister to changes of the filter selection. - //add buffer because it clears selection first and this would cause it to fire twice - this.genreFilter.getSelectionModel().on('selectionchange', this.onGenreFilterChange, this, {buffer: 1}); - - // Attach listener for running the module - this.on("afterrender", this.runModule, this); - }, - - // Fired when the Genre filter selection changes - onGenreFilterChange: function (sm) { - - var selectedRecords = sm.getSelections(), - ids = selectedRecords.column('id'); //column is a special GO method that get's all the id's from the records in an array. - - this.artistGrid.store.baseParams.filter.genres = ids; - this.artistGrid.store.load(); - }, - - // Fired when the module panel is rendered. - runModule: function () { - // when this panel renders, load the genres and artists. - this.genreFilter.store.load(); - this.artistGrid.store.load(); - }, - - // Fires when an artist is double clicked in the grid. - onGridDblClick: function (grid, rowIndex, e) { - - //check permissions - var record = grid.getStore().getAt(rowIndex); - if (record.get('permissionLevel') < go.permissionLevels.write) { - return; - } - - // Show dialog - var dlg = new go.modules.tutorial.music.ArtistDialog(); - dlg.load(record.id).show(); - }, - - // Fires when enter is pressed and a grid row is focussed - onGridKeyPress: function (e) { - if (e.keyCode != e.ENTER) { - return; - } - - var record = this.artistGrid.getSelectionModel().getSelected(); - if (!record) { - return; - } - - if (record.get('permissionLevel') < go.permissionLevels.write) { - return; - } - - var dlg = new go.modules.tutorial.music.ArtistDialog(); - dlg.load(record.id).show(); - - } - -}); \ No newline at end of file diff --git a/music/views/extjs3/Module.js b/music/views/extjs3/Module.js deleted file mode 100644 index 46cc24a..0000000 --- a/music/views/extjs3/Module.js +++ /dev/null @@ -1,27 +0,0 @@ -go.Modules.register("tutorial", "music", { - mainPanel: "go.modules.tutorial.music.MainPanel", - - //The title is shown in the menu and tab bar - title: t("Music"), - - //All module entities must be defined here. Stores will be created for them. - entities: [ - "Genre", - { - name: "Artist", - relations: { - creator: {store: "User", fk: "createdBy"}, - modifier: {store: "User", fk: "createdBy"}, - - // 'albums' is a property of artist and has a nested relation. - albums: { - genre: {store: "Genre", fk: "genreId"} - } - } - } - ], - - //Put code to initialize the module here after the user is authenticated - //and has access to the module. - initModule: function () {} -}); diff --git a/music/views/extjs3/scripts.txt b/music/views/extjs3/scripts.txt deleted file mode 100644 index 2281d25..0000000 --- a/music/views/extjs3/scripts.txt +++ /dev/null @@ -1,7 +0,0 @@ -Module.js -MainPanel.js -GenreFilter.js -ArtistGrid.js -GenreCombo.js -ArtistDialog.js -ArtistDetail.js \ No newline at end of file diff --git a/music/views/extjs3/themes/default/style.css b/music/views/extjs3/themes/default/style.css deleted file mode 100644 index e69de29..0000000 diff --git a/music/views/goui/package-lock.json b/music/views/goui/package-lock.json new file mode 100644 index 0000000..0504030 --- /dev/null +++ b/music/views/goui/package-lock.json @@ -0,0 +1,1349 @@ +{ + "name": "goui", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "concurrently": "latest", + "esbuild": "latest", + "sass": "latest", + "typescript": "latest" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/watcher/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@parcel/watcher/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@parcel/watcher/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@parcel/watcher/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@parcel/watcher/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chokidar/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/concurrently": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", + "integrity": "sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/sass": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", + "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-color/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yargs/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/yargs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/music/views/goui/package.json b/music/views/goui/package.json new file mode 100644 index 0000000..ea03982 --- /dev/null +++ b/music/views/goui/package.json @@ -0,0 +1,19 @@ +{ + "type": "module", + "devDependencies": { + "concurrently": "latest", + "esbuild": "latest", + "sass": "latest", + "typescript": "latest" + }, + + "scripts": { + "start": "concurrently --kill-others \"npm run start:ts\" \"npm run start:sass\"", + "start:ts": "npx esbuild script/Index.ts --external:../../../../../../views/goui/* --bundle --watch --sourcemap --format=esm --target=esnext --outdir=dist", + "start:sass": "npx sass --watch style:dist", + + "build": "npm run build:sass && npm run build:ts", + "build:ts": "npx esbuild script/Index.ts --external:../../../../../../views/goui/* --bundle --minify --sourcemap --format=esm --target=esnext --outdir=dist", + "build:sass": "npx sass --style=compressed style:dist" + } +} diff --git a/music/views/goui/script/Artist.ts b/music/views/goui/script/Artist.ts new file mode 100644 index 0000000..e5b679e --- /dev/null +++ b/music/views/goui/script/Artist.ts @@ -0,0 +1,19 @@ +import {BaseEntity, DateTime, EntityID} from "@intermesh/goui"; + +export interface Artist extends BaseEntity { + name: string, + genreId: number, + photo: string|null, + createdBy: EntityID, + modifiedBy: EntityID, + createdAt: DateTime, + modifiedAt: DateTime, + albums: Album[] +} + +export interface Album { + name: string, + releaseDate: DateTime, + genreId: EntityID, + artistId: EntityID +} \ No newline at end of file diff --git a/music/views/goui/script/ArtistDetail.ts b/music/views/goui/script/ArtistDetail.ts new file mode 100644 index 0000000..87b65bb --- /dev/null +++ b/music/views/goui/script/ArtistDetail.ts @@ -0,0 +1,160 @@ +import { + Button, + Component, + DataSourceForm, + avatar, + btn, + column, + comp, + datasourceform, + datecolumn, + fieldset, + store, + t, + table, + Table, + tbar, + menu, + hr +} from "@intermesh/goui"; +import {DetailPanel, img, jmapds, router} from "@intermesh/groupoffice-core"; +import {ArtistWindow} from "./ArtistWindow.js"; +import {Artist} from "./Artist.js"; + +export class ArtistDetail extends DetailPanel { + private form: DataSourceForm; + private avatarContainer: Component; + private albumsTable: Table; + + constructor() { + super("Artist"); + this.width = 500; + this.itemId = "detail"; + this.stateId = "music-detail"; + + this.scroller.items.add( + this.form = datasourceform({ + dataSource: jmapds("Artist") + }, + + comp({cls: "card"}, + tbar({}, + this.titleCmp = comp({tagName: "h3", flex: 1}), + ), + comp({cls: "hflow", flex: 1}, + this.avatarContainer = comp({ + cls: "go-detail-view-avatar pad", + itemId: "avatar-container" + }), + ), + ) + ), + + fieldset({legend: t("Albums")}, + tbar({}, "->", btn({icon: "add", cls: "primary", text: t("Add"), handler:() => { + // TODO + // const w = new AlbumWindow(); + // w.on("close", async () => { + // this.load(this.entity!.id) + // }); + // w.show(); + }})), + this.albumsTable = table({ + fitParent: true, + // headers: false, + store: store({ + data: [] + }), + columns: [ + column({ + id: "id", + hidden: true, + }), + column({ + id: "name", + header: t("Title"), + resizable: true, + sortable: true + }), + datecolumn({ + id: "releaseDate", + header: t("Release date"), + sortable: true + }), + column({ + resizable: true, + id: "genreId", + header: t("Genre"), + renderer: async (v) => { + const g = await jmapds("Genre").single(v); + return g!.name; + } + }), + column({ + resizable: false, + // sticky: true, + width: 32, + id: "btn", + renderer: (columnValue: any, record, td, table, rowIndex) => { + + return btn({ + icon: "more_vert", menu: menu({}, btn({ + icon: "edit", text: t("Edit"), handler: async (_btn) => { + // TODO... + // const dlg = new go.modules.community.tasks.AlbumDialog({ + // redirectOnSave: false + // }); + // + // const album = table.store.get(rowIndex)!; + // + // dlg.load(album.id); + // dlg.show(); + } + }), hr(), btn({ + icon: "delete", text: t("Delete"), handler: async (btn) => { + // TODO... + } + })) + }) + } + }) + ] + }) + ) + ); + + this.toolbar.items.add( + btn({ + icon: "edit", + title: t("Edit"), + handler: (button, ev) => { + const dlg = new ArtistWindow(); + void dlg.load(this.entity!.id); + dlg.show(); + } + }), + btn({ + icon: "delete", + title: t("Delete"), + handler: () => { + jmapds("Artist").destroy(this.entity!.id).then(() => { + router.goto("music"); + }) + } + }) + ) + this.on("load", (pnl, entity) => { + this.title = entity.name + if (entity!.photo) { + pnl.avatarContainer.items.replace(img({ + cls: "goui-avatar", + blobId: entity.photo, + title: entity.name + })); + } else { + pnl.avatarContainer.items.replace(avatar({cls: "goui-avatar", displayName: entity.name})); + } + this.albumsTable.store.loadData(entity.albums, false); + }) + } +} \ No newline at end of file diff --git a/music/views/goui/script/ArtistTable.ts b/music/views/goui/script/ArtistTable.ts new file mode 100644 index 0000000..05a8512 --- /dev/null +++ b/music/views/goui/script/ArtistTable.ts @@ -0,0 +1,64 @@ +import {BaseEntity, DataSourceStore, Table, avatar, column, comp, datasourcestore, t } from "@intermesh/goui"; +import { JmapDataSource, img, jmapds } from "@intermesh/groupoffice-core"; + +interface Artist extends BaseEntity { + name: string, +} + +export class ArtistTable extends Table { + constructor() { + const store = datasourcestore, Artist>({ + dataSource: jmapds("Artist"), + queryParams: { + limit: 0, + filter: { + permissionLevel: 5 + } + }, + sort: [{property: "name", isAscending: true}] + }); + + const columns = [ + column({ + id: "id", + hidden: true, + sortable: true, + }), + column({ + header: t("Photo"), + id: "photo", + resizable: false, + width: 80, + renderer: (v, record) => { + const c = comp({ + itemId: "avatar-container" + }); + if (v) { + c.items.add(img({ + cls: "goui-avatar", + blobId: v, + title: record.name + })) + } else { + c.items.add(avatar({displayName: record.name})); + } + return c; + } + }), + column({ + header: t("Name"), + id: "name", + resizable: true, + sortable: true + }), + + ]; + + super(store, columns); + this.fitParent = true; + this.rowSelectionConfig = { + multiSelect: true + }; + } + +} \ No newline at end of file diff --git a/music/views/goui/script/ArtistWindow.ts b/music/views/goui/script/ArtistWindow.ts new file mode 100644 index 0000000..0acb925 --- /dev/null +++ b/music/views/goui/script/ArtistWindow.ts @@ -0,0 +1,47 @@ +import {Component, comp, fieldset, t, textfield} from "@intermesh/goui"; +import {FormWindow, router} from "@intermesh/groupoffice-core"; + +export class ArtistWindow extends FormWindow { + private avatarComp: Component; + constructor() { + super("Artist"); + + this.title = t("Artist"); + + this.stateId = "artist-dialog"; + this.maximizable = true; + this.resizable = true; + this.width = 640; + + this.form.on('beforesave', (form, data) => { + debugger; + + }); + + this.form.on("save", (form, data, isNew) => { + if (isNew) { + router.goto("artist/" + data.id); + } + }) + + this.generalTab.items.add( + fieldset({}, + + textfield({ + name: "name", + label: t("Name"), + required: true + }), + comp({ + }, this.avatarComp = new go.form.ImageField({ + name: "photo" + })) + ) + ) + + // this.addCustomFields(); + + } + + +} \ No newline at end of file diff --git a/music/views/goui/script/GenreTable.ts b/music/views/goui/script/GenreTable.ts new file mode 100644 index 0000000..cc4119d --- /dev/null +++ b/music/views/goui/script/GenreTable.ts @@ -0,0 +1,39 @@ +import {BaseEntity, DataSourceStore, Table, checkboxselectcolumn, column, datasourcestore, t} from "@intermesh/goui"; +import {JmapDataSource, jmapds} from "@intermesh/groupoffice-core"; + +interface Genre extends BaseEntity { + name: string, +} + +export class GenreTable extends Table { + constructor() { + const store = datasourcestore, Genre>({ + dataSource: jmapds("Genre"), + queryParams: { + limit: 0, + filter: { + permissionLevel: 5 + } + }, + sort: [{property: "name", isAscending: true}] + }); + + const columns = [ + checkboxselectcolumn(), + column({ + header: t("Name"), + id: "name", + resizable: true, + width: 312, + sortable: true + }) + ]; + + super(store, columns); + this.fitParent = true; + + this.rowSelectionConfig = { + multiSelect: true + }; + } +} \ No newline at end of file diff --git a/music/views/goui/script/Index.ts b/music/views/goui/script/Index.ts new file mode 100644 index 0000000..d3b7a5d --- /dev/null +++ b/music/views/goui/script/Index.ts @@ -0,0 +1,35 @@ +import {client, modules, router} from "@intermesh/groupoffice-core"; +import {Main} from "./Main.js"; +import {t, translate, EntityID} from "@intermesh/goui"; + +modules.register( { + package: "tutorial", + name: "music", + async init () { + + client.on("authenticated", (client, session) => { + if(!session.capabilities["go:tutorial:music"]) { + // User has no access to this module + return; + } + + translate.load(GO.lang.core.core, "core", "core"); + translate.load(GO.lang.tutorial.music, "tutorial", "music"); + + const mainPanel = new Main(); + + router.add(/^music\/(\d+)$/, (id: EntityID) => { + modules.openMainPanel("music"); + mainPanel.load(id); + }); + + router.add(/^music$/, () => { + modules.openMainPanel("music"); + }); + + modules.addMainPanel( "tutorial", "music", "music", t("Music"), () => { + return mainPanel; + }); + }); + } +}); \ No newline at end of file diff --git a/music/views/goui/script/Main.ts b/music/views/goui/script/Main.ts new file mode 100644 index 0000000..a7b4284 --- /dev/null +++ b/music/views/goui/script/Main.ts @@ -0,0 +1,201 @@ +import { + btn, + comp, + Component, + EntityID, + mstbar, + Notifier, + paginator, + searchbtn, + splitter, + t, + tbar +} from "@intermesh/goui"; +import { + authManager, + router, + FilterCondition, +} from "@intermesh/groupoffice-core"; +import {ArtistTable} from "./ArtistTable.js"; +import {GenreTable} from "./GenreTable.js"; +import { ArtistDetail } from "./ArtistDetail.js"; +import {ArtistWindow} from "./ArtistWindow.js"; + +export class Main extends Component { + private artistTable: ArtistTable; + + + private genreTable!: GenreTable; + private west: Component; + private center: Component; + private east: ArtistDetail; + + constructor() { + super("section"); + + this.id = "music"; + this.cls = "vbox fit"; + this.on("render", async () => { + try { + await authManager.requireLogin(); + } catch (e) { + console.warn(e); + Notifier.error(t("Login is required on this page")); + } + + await this.genreTable.store.load(); + await this.artistTable.store.load(); + }); + + + this.artistTable = new ArtistTable(); + this.artistTable.on("navigate", async (table: ArtistTable, rowIndex: number) => { + await router.goto("music/" + table.store.get(rowIndex)!.id); + }); + + this.west = this.createWest(); + this.items.add( + comp({ + flex: 1, cls: "hbox mobile-cards" + }, + + this.west, + + splitter({ + stateId: "music-splitter-west", + resizeComponentPredicate: this.west + }), + + this.center = comp({ + cls: 'active vbox', + itemId: 'table-container', + flex: 1, + style: { + minWidth: "365px", //for the resizer's boundaries + maxWidth: "850px" + } + }, + + tbar({}, + btn({ + cls: "for-small-device", + title: t("Menu"), + icon: "menu", + handler: (button, ev) => { + this.activatePanel(this.west); + } + }), + + '->', + + searchbtn({ + listeners: { + input: (sender, text) => { + + (this.artistTable.store.queryParams.filter as FilterCondition).text = text; + this.artistTable.store.load(); + + } + } + }), + + mstbar({table: this.artistTable}), + + btn({ + itemId: "add", + icon: "add", + cls: "filled primary", + handler: async () => { + const w = new ArtistWindow(); + w.on("close", async () => { + debugger; + }); + w.show(); + + } + }) + ), + + comp({ + flex: 1, + stateId: "music", + cls: "scroll border-top main" + }, + this.artistTable + ), + + + paginator({ + store: this.artistTable.store + }) + ), + + + splitter({ + stateId: "music-splitter", + resizeComponentPredicate: "table-container" + }), + + this.east = new ArtistDetail() + ) + ); + } + + private activatePanel(active: Component) { + this.center.el.classList.remove("active"); + this.east.el.classList.remove("active"); + this.west.el.classList.remove("active"); + + active.el.classList.add("active"); + } + + private createWest(): Component { + this.genreTable = new GenreTable(); + this.genreTable.rowSelectionConfig = { + multiSelect: true, + listeners: { + selectionchange: (tableRowSelect) => { + const genreIds = tableRowSelect.selected.map((index: number) => tableRowSelect.list.store.get(index)!.id); + (this.artistTable.store.queryParams.filter as FilterCondition).genres = genreIds; + this.artistTable.store.load(); + } + } + } + + return comp({ + cls: "vbox scroll", + width: 300 + }, + tbar({ + cls: "border-bottom" + }, + comp({ + tagName: "h3", + text: t("Genre"), + flex: 1 + }), + '->', + btn({ + cls: "for-small-device", + title: t("Close"), + icon: "close", + handler: (button, ev) => { + this.activatePanel(this.center); + } + }) + ), + this.genreTable + ); + } + + async load(id?: EntityID) { + if(id) { + void this.east.load(id); + this.activatePanel(this.east); + } else { + this.activatePanel(this.center); + } + + } + +} diff --git a/music/views/goui/style/style.scss b/music/views/goui/style/style.scss new file mode 100644 index 0000000..6958a5c --- /dev/null +++ b/music/views/goui/style/style.scss @@ -0,0 +1,11 @@ +.go-detail-view-avatar { + min-width: 240px; + //min-height: 240px; + text-align: center; + display: flex; + justify-content: center; + & > .goui-avatar { + width: 120px !important; + height: 120px !important; + } +} \ No newline at end of file diff --git a/music/views/goui/tsconfig.json b/music/views/goui/tsconfig.json new file mode 100644 index 0000000..81a6670 --- /dev/null +++ b/music/views/goui/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../../../../views/goui/tsconfig.module.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./dist" + } +} \ No newline at end of file From bf1099806c51ab95381c4dfedfac09066301046c Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Mon, 25 Nov 2024 15:33:39 +0100 Subject: [PATCH 09/17] Fix models, add bio and active fields to details --- music/model/Album.php | 2 + music/model/Artist.php | 19 ++- music/views/goui/script/ArtistDetail.ts | 169 ++++++++++++++---------- 3 files changed, 110 insertions(+), 80 deletions(-) diff --git a/music/model/Album.php b/music/model/Album.php index 8b993c2..fc10188 100644 --- a/music/model/Album.php +++ b/music/model/Album.php @@ -33,6 +33,8 @@ final class Album extends Property { /** @var int */ public int $genreId; + public array $reviews; + protected static function defineMapping(): Mapping { return parent::defineMapping() ->addTable("tutorial_music_album", "album") diff --git a/music/model/Artist.php b/music/model/Artist.php index 62aff2f..8a31aec 100644 --- a/music/model/Artist.php +++ b/music/model/Artist.php @@ -63,10 +63,10 @@ final class Artist extends Entity /** @var array */ public array $albums; - public array $reviews; +// public array $reviews; /** @var int */ - protected int $albumCount; +// protected int $albumCount; public bool $active = true; @@ -76,7 +76,7 @@ protected static function defineMapping(): Mapping { return parent::defineMapping() ->addTable("tutorial_music_artist", "artist") - ->addArray('albums', Album::class, ['id' => 'artistId']); + ->addArray('albums', Album::class, ['id' => 'artistId'], ['orderBy' => 'releaseDate']); } /** @@ -108,13 +108,18 @@ protected static function defineFilters(): Filters }); } +// protected function getReviews() : array +// { +// return $this->reviews; +// } + /** * The album count is simply the number of albums as per the artist-album relation * * @return int */ - public function getAlbumCount() :int - { - return count($this->albums); - } +// public function getAlbumCount() :int +// { +// return count($this->albums); +// } } \ No newline at end of file diff --git a/music/views/goui/script/ArtistDetail.ts b/music/views/goui/script/ArtistDetail.ts index 87b65bb..f488688 100644 --- a/music/views/goui/script/ArtistDetail.ts +++ b/music/views/goui/script/ArtistDetail.ts @@ -14,8 +14,9 @@ import { table, Table, tbar, - menu, - hr + menu, + hr, + displayfield } from "@intermesh/goui"; import {DetailPanel, img, jmapds, router} from "@intermesh/groupoffice-core"; import {ArtistWindow} from "./ArtistWindow.js"; @@ -41,85 +42,106 @@ export class ArtistDetail extends DetailPanel { tbar({}, this.titleCmp = comp({tagName: "h3", flex: 1}), ), - comp({cls: "hflow", flex: 1}, + comp({cls: "hbox", flex: 1}, this.avatarContainer = comp({ cls: "go-detail-view-avatar pad", itemId: "avatar-container" }), ), - ) + comp({cls: "pad flow"}, + fieldset({}, + comp({cls: "vbox", flex: 1}, + displayfield({ + icon: "person_alert", + name: "active", + label: t("Active"), + renderer: (v) => { + return v ? t("Yes") : t("No"); + } + }), + displayfield({ + icon: "book", + name: "bio", + label: t("Biography"), + }) + ), + ), + ), + ), ), fieldset({legend: t("Albums")}, - tbar({}, "->", btn({icon: "add", cls: "primary", text: t("Add"), handler:() => { - // TODO - // const w = new AlbumWindow(); - // w.on("close", async () => { - // this.load(this.entity!.id) - // }); - // w.show(); - }})), - this.albumsTable = table({ - fitParent: true, - // headers: false, - store: store({ - data: [] - }), - columns: [ - column({ - id: "id", - hidden: true, - }), - column({ - id: "name", - header: t("Title"), - resizable: true, - sortable: true - }), - datecolumn({ - id: "releaseDate", - header: t("Release date"), - sortable: true + tbar({}, "->", btn({ + icon: "add", cls: "primary", text: t("Add"), handler: () => { + // TODO + // const w = new AlbumWindow(); + // w.on("close", async () => { + // this.load(this.entity!.id) + // }); + // w.show(); + } + })), + this.albumsTable = table({ + fitParent: true, + // headers: false, + store: store({ + data: [] }), - column({ - resizable: true, - id: "genreId", - header: t("Genre"), - renderer: async (v) => { - const g = await jmapds("Genre").single(v); - return g!.name; - } - }), - column({ - resizable: false, - // sticky: true, - width: 32, - id: "btn", - renderer: (columnValue: any, record, td, table, rowIndex) => { + columns: [ + column({ + id: "id", + hidden: true, + }), + column({ + id: "name", + header: t("Title"), + resizable: true, + sortable: true + }), + datecolumn({ + id: "releaseDate", + header: t("Release date"), + sortable: true + }), + column({ + resizable: true, + id: "genreId", + header: t("Genre"), + renderer: async (v) => { + const g = await jmapds("Genre").single(v); + return g!.name; + } + }), + column({ + resizable: false, + // sticky: true, + width: 32, + id: "btn", + renderer: (columnValue: any, record, td, table, rowIndex) => { - return btn({ - icon: "more_vert", menu: menu({}, btn({ - icon: "edit", text: t("Edit"), handler: async (_btn) => { - // TODO... - // const dlg = new go.modules.community.tasks.AlbumDialog({ - // redirectOnSave: false - // }); - // - // const album = table.store.get(rowIndex)!; - // - // dlg.load(album.id); - // dlg.show(); - } - }), hr(), btn({ - icon: "delete", text: t("Delete"), handler: async (btn) => { - // TODO... - } - })) - }) - } - }) - ] - }) + return btn({ + icon: "more_vert", menu: menu({}, btn({ + icon: "edit", text: t("Edit"), handler: async (_btn) => { + // TODO... + // const dlg = new go.modules.community.tasks.AlbumDialog({ + // redirectOnSave: false + // }); + // + // const album = table.store.get(rowIndex)!; + // + // dlg.load(album.id); + // dlg.show(); + } + }), hr(), btn({ + icon: "delete", text: t("Delete"), handler: async (btn) => { + // TODO... + } + })) + }) + } + }) + ] + }) ) ); @@ -133,7 +155,7 @@ export class ArtistDetail extends DetailPanel { dlg.show(); } }), - btn({ + btn({ icon: "delete", title: t("Delete"), handler: () => { @@ -144,7 +166,8 @@ export class ArtistDetail extends DetailPanel { }) ) this.on("load", (pnl, entity) => { - this.title = entity.name + this.title = entity.name; + void this.form.load(entity.id); if (entity!.photo) { pnl.avatarContainer.items.replace(img({ cls: "goui-avatar", From a5b3af582462d454fc4b95d96acd987699f3c585 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Tue, 26 Nov 2024 08:45:29 +0100 Subject: [PATCH 10/17] Interim album window --- music/model/Album.php | 5 +- music/views/goui/script/AlbumWindow.ts | 69 +++++++++++++++++++++++++ music/views/goui/script/ArtistDetail.ts | 24 ++++----- 3 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 music/views/goui/script/AlbumWindow.ts diff --git a/music/model/Album.php b/music/model/Album.php index fc10188..bd3b247 100644 --- a/music/model/Album.php +++ b/music/model/Album.php @@ -3,6 +3,7 @@ use go\core\orm\Mapping; use go\core\orm\Property; +use go\core\util\DateTime; /** * Album model @@ -27,8 +28,8 @@ final class Album extends Property { /** @var string */ public string $name; - /** @var \go\core\util\DateTime */ - public \go\core\util\DateTime $releaseDate; + /** @var DateTime */ + public DateTime $releaseDate; /** @var int */ public int $genreId; diff --git a/music/views/goui/script/AlbumWindow.ts b/music/views/goui/script/AlbumWindow.ts new file mode 100644 index 0000000..8efae73 --- /dev/null +++ b/music/views/goui/script/AlbumWindow.ts @@ -0,0 +1,69 @@ +import {Component, EntityID, Form, Notifier, Window, btn, combobox, comp, datefield, fieldset, form, t, tbar, textfield} from "@intermesh/goui"; +import { jmapds } from "@intermesh/groupoffice-core"; +export class AlbumWindow extends Window { + + private artistId: EntityID; + private form: Form; + constructor(artistId: EntityID) { + super(); + this.artistId = artistId; + Object.assign(this, { + title: t("New album"), + width: 800, + height: 500, + modal: true, + resizable: false, + maximizable: false + } + ); + this.form = form({ + flex: 1, + cls: "vbox", + handler: (albumfrm) => { + // TODO: Save new album or update current + debugger; + const v = albumfrm.value; + v.artistId = artistId; + console.log(v); + jmapds("Artist").update(artistId, {albums: [v]}).then((result) => {console.log(result);this.close();}).catch((e) => Notifier.error(e)) + } + }, + fieldset({ + cls: "flow scroll", + flex: 1 + }, + + comp({cls: "vbox gap"}, + + textfield({ + label: t("Title"), + name: "name", + required: true, + }), + + datefield({ + name: "releaseDate", + required: true, + label: t("Release date"), + }), + + combobox({ + name: "genreId", + label: t("Genre"), + required: true, + dataSource: jmapds("Genre"), + }) + ), + + ), + tbar({}, "->", btn({type: "submit", text: t("Save")})) + ); + + this.items.add(this.form); + } + + public load(record: any) { + this.title = record.name; + this.form.value = record; + } +} \ No newline at end of file diff --git a/music/views/goui/script/ArtistDetail.ts b/music/views/goui/script/ArtistDetail.ts index f488688..f1c7368 100644 --- a/music/views/goui/script/ArtistDetail.ts +++ b/music/views/goui/script/ArtistDetail.ts @@ -20,6 +20,7 @@ import { } from "@intermesh/goui"; import {DetailPanel, img, jmapds, router} from "@intermesh/groupoffice-core"; import {ArtistWindow} from "./ArtistWindow.js"; +import {AlbumWindow} from "./AlbumWindow.js"; import {Artist} from "./Artist.js"; export class ArtistDetail extends DetailPanel { @@ -74,11 +75,11 @@ export class ArtistDetail extends DetailPanel { tbar({}, "->", btn({ icon: "add", cls: "primary", text: t("Add"), handler: () => { // TODO - // const w = new AlbumWindow(); - // w.on("close", async () => { - // this.load(this.entity!.id) - // }); - // w.show(); + const w = new AlbumWindow(this.entity!.id); + w.on("close", async () => { + this.load(this.entity!.id) + }); + w.show(); } })), this.albumsTable = table({ @@ -122,15 +123,10 @@ export class ArtistDetail extends DetailPanel { return btn({ icon: "more_vert", menu: menu({}, btn({ icon: "edit", text: t("Edit"), handler: async (_btn) => { - // TODO... - // const dlg = new go.modules.community.tasks.AlbumDialog({ - // redirectOnSave: false - // }); - // - // const album = table.store.get(rowIndex)!; - // - // dlg.load(album.id); - // dlg.show(); + const dlg = new AlbumWindow(this.entity!.id); + const album = table.store.get(rowIndex)!; + dlg.load(album); + dlg.show(); } }), hr(), btn({ icon: "delete", text: t("Delete"), handler: async (btn) => { From 54a0643bb9cf803c258ad354661f77b7c4eac1f1 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Tue, 26 Nov 2024 10:07:02 +0100 Subject: [PATCH 11/17] Sort option removed - will do client side --- music/model/Artist.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/music/model/Artist.php b/music/model/Artist.php index 8a31aec..8bc3a0d 100644 --- a/music/model/Artist.php +++ b/music/model/Artist.php @@ -76,7 +76,8 @@ protected static function defineMapping(): Mapping { return parent::defineMapping() ->addTable("tutorial_music_artist", "artist") - ->addArray('albums', Album::class, ['id' => 'artistId'], ['orderBy' => 'releaseDate']); +// ->addArray('albums', Album::class, ['id' => 'artistId'], ['orderBy' => 'releaseDate']); + ->addArray('albums', Album::class, ['id' => 'artistId']); } /** From 481a4381999599e4fffb734a37730120b5b4248a Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Tue, 26 Nov 2024 10:59:40 +0100 Subject: [PATCH 12/17] Album window first working version --- music/views/goui/script/AlbumWindow.ts | 28 ++++++++++++++++--------- music/views/goui/script/Artist.ts | 3 ++- music/views/goui/script/ArtistDetail.ts | 5 ++--- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/music/views/goui/script/AlbumWindow.ts b/music/views/goui/script/AlbumWindow.ts index 8efae73..3362322 100644 --- a/music/views/goui/script/AlbumWindow.ts +++ b/music/views/goui/script/AlbumWindow.ts @@ -1,12 +1,14 @@ -import {Component, EntityID, Form, Notifier, Window, btn, combobox, comp, datefield, fieldset, form, t, tbar, textfield} from "@intermesh/goui"; +import {EntityID, Form, Notifier, Window, btn, combobox, comp, datefield, fieldset, form, t, tbar, textfield} from "@intermesh/goui"; import { jmapds } from "@intermesh/groupoffice-core"; +import {Album, Artist} from "./Artist"; export class AlbumWindow extends Window { - private artistId: EntityID; - private form: Form; - constructor(artistId: EntityID) { + private entity: Artist; + private albumId: EntityID | undefined; + private readonly form: Form; + constructor(artist: Artist) { super(); - this.artistId = artistId; + this.entity = artist; Object.assign(this, { title: t("New album"), width: 800, @@ -20,12 +22,17 @@ export class AlbumWindow extends Window { flex: 1, cls: "vbox", handler: (albumfrm) => { - // TODO: Save new album or update current - debugger; const v = albumfrm.value; - v.artistId = artistId; - console.log(v); - jmapds("Artist").update(artistId, {albums: [v]}).then((result) => {console.log(result);this.close();}).catch((e) => Notifier.error(e)) + v.artistId = this.entity.id; + if(this.albumId) { + const curr = this.entity.albums.find((a) => a.id === this.albumId); + Object.assign(curr!, v); + } else { + this.entity.albums.push(v as Album); + } + jmapds("Artist").update(this.entity.id, {albums: this.entity.albums}) + .then((result) => {console.log(result);this.close();}) + .catch((e) => Notifier.error(e)) } }, fieldset({ @@ -64,6 +71,7 @@ export class AlbumWindow extends Window { public load(record: any) { this.title = record.name; + this.albumId = record.id; this.form.value = record; } } \ No newline at end of file diff --git a/music/views/goui/script/Artist.ts b/music/views/goui/script/Artist.ts index e5b679e..2091d10 100644 --- a/music/views/goui/script/Artist.ts +++ b/music/views/goui/script/Artist.ts @@ -15,5 +15,6 @@ export interface Album { name: string, releaseDate: DateTime, genreId: EntityID, - artistId: EntityID + artistId: EntityID, + id: EntityID } \ No newline at end of file diff --git a/music/views/goui/script/ArtistDetail.ts b/music/views/goui/script/ArtistDetail.ts index f1c7368..b19d417 100644 --- a/music/views/goui/script/ArtistDetail.ts +++ b/music/views/goui/script/ArtistDetail.ts @@ -74,8 +74,7 @@ export class ArtistDetail extends DetailPanel { fieldset({legend: t("Albums")}, tbar({}, "->", btn({ icon: "add", cls: "primary", text: t("Add"), handler: () => { - // TODO - const w = new AlbumWindow(this.entity!.id); + const w = new AlbumWindow(this.entity!); w.on("close", async () => { this.load(this.entity!.id) }); @@ -123,7 +122,7 @@ export class ArtistDetail extends DetailPanel { return btn({ icon: "more_vert", menu: menu({}, btn({ icon: "edit", text: t("Edit"), handler: async (_btn) => { - const dlg = new AlbumWindow(this.entity!.id); + const dlg = new AlbumWindow(this.entity!); const album = table.store.get(rowIndex)!; dlg.load(album); dlg.show(); From ef332468d9e37c664d4323623d228d9a50fa10a7 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Tue, 26 Nov 2024 13:15:52 +0100 Subject: [PATCH 13/17] definitively delete ExtJS3 code, update README --- README.md | 15 -- music/views/extjs3/ArtistDetail.js | 180 -------------- music/views/extjs3/ArtistDialog.js | 80 ------ music/views/extjs3/ArtistGrid.js | 118 --------- music/views/extjs3/GenreCombo.js | 22 -- music/views/extjs3/GenreFilter.js | 132 ---------- music/views/extjs3/MainPanel.js | 233 ------------------ music/views/extjs3/Module.js | 36 --- music/views/extjs3/ReviewDialog.js | 61 ----- music/views/extjs3/ReviewsModal.js | 197 --------------- music/views/extjs3/scripts.txt | 9 - .../extjs3/themes/default/src/style.scss | 7 - music/views/extjs3/themes/default/style.css | 9 - .../views/extjs3/themes/default/style.css.map | 1 - music/views/goui/script/Artist.ts | 2 +- music/views/goui/script/ArtistDetail.ts | 29 ++- music/views/goui/style/style.scss | 1 - 17 files changed, 21 insertions(+), 1111 deletions(-) delete mode 100644 music/views/extjs3/ArtistDetail.js delete mode 100644 music/views/extjs3/ArtistDialog.js delete mode 100644 music/views/extjs3/ArtistGrid.js delete mode 100644 music/views/extjs3/GenreCombo.js delete mode 100644 music/views/extjs3/GenreFilter.js delete mode 100644 music/views/extjs3/MainPanel.js delete mode 100644 music/views/extjs3/Module.js delete mode 100644 music/views/extjs3/ReviewDialog.js delete mode 100644 music/views/extjs3/ReviewsModal.js delete mode 100644 music/views/extjs3/scripts.txt delete mode 100644 music/views/extjs3/themes/default/src/style.scss delete mode 100644 music/views/extjs3/themes/default/style.css delete mode 100644 music/views/extjs3/themes/default/style.css.map diff --git a/README.md b/README.md index 66ca060..9b3eff6 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,7 @@ This code belongs to the Group-Office developer tutorial: https://groupoffice.readthedocs.io/en/latest/developer/index.html -<<<<<<< HEAD Clone this as "tutorial" inside the `go/modules` folder: -======= -Clone this as "tutorial" inside the go/modules folder: ->>>>>>> upstream/master -``` cd go/modules git clone https://github.com/Intermesh/groupoffice-tutorial.git tutorial -<<<<<<< HEAD -```` -======= -```` - -## TODO - -- Update compatibility to >6.4 -- Refactor into GOUI ->>>>>>> upstream/master diff --git a/music/views/extjs3/ArtistDetail.js b/music/views/extjs3/ArtistDetail.js deleted file mode 100644 index 7a2fe20..0000000 --- a/music/views/extjs3/ArtistDetail.js +++ /dev/null @@ -1,180 +0,0 @@ -go.modules.tutorial.music.ArtistDetail = Ext.extend(go.detail.Panel, { - - // The entity store is connected. The detail view is automatically updated. - entityStore: "Artist", - - //set to true to enable state saving - stateful: false, - stateId: 'music-contact-detail', - - // Fetch these relations for this view - relations: ["albums.genre", "creator"], - - initComponent: function () { - this.tbar = this.initToolbar(); - - // Render a 'new review' modal - this.addReviewModal = function (v) { - var dlg = new go.modules.tutorial.music.ReviewDialog(); - dlg.setValues({albumId:v.id}); - dlg.show(); - }; - - // Render all reviews for the current album in a window (which is offered as a modal) - this.showReviewsModal = function(v) { - var dlg = new go.modules.tutorial.music.ReviewsModal(); - dlg.store.setFilter('albumId', {albumId: v.id}); - dlg.store.load(); - dlg.show(); - }; - Ext.apply(this, { - // all items are updated automatically if they have a "tpl" (Ext.XTemplate) property or an "onLoad" function. The panel is passed as argument. - items: [ - - //Artist name component - { - cls: 'content', - xtype: 'box', - tpl: '

{name}

' - }, - - //Render the avatar - { - xtype: "box", - cls: "content", - tpl: new Ext.XTemplate('
\ -
', - { - getStyle: function (photoBlobId) { - return photoBlobId ? 'background-image: url(' + go.Jmap.downloadUrl(photoBlobId) + ')"' : ""; - } - }) - }, - - // Albums component, render number of reviews - { - collapsible: true, - title: t("Albums"), - xtype: "panel", - listeners: { - scope: this, - afterrender: function(box) { - box.getEl().on('click', function(e){ - - //don't execute when user selects text - if(window.getSelection().toString().length > 0) { - return; - } - - var container = box.getEl().dom.childNodes[1], - item = e.getTarget("a", box.getEl()), - i = Array.prototype.indexOf.call(container.getElementsByTagName("a"), item); - if(i >=0) { - var album = go.util.Object.convertMapToArray(this.data.albums,'id')[i]; - if(album.reviews.length > 0) { - this.showReviewsModal(album); - } else { - this.addReviewModal(album); - } - } - }, this); - } - }, - tpl: new Ext.XTemplate('
\ - \ -

album\ - {name}\ - \ -

\ -
\ -
', - { - displayNumReviews: function(v){ - v = v || null; - if(v === null) { - return ""; - } else if(v.length == 0) { - return "- " + t("Write a Review")+ ""; - } else { - return "- " +v.length+" " + t("Reviews")+ "" - } - } - }) - } - ] - }); - - - go.modules.tutorial.music.ArtistDetail.superclass.initComponent.call(this); - - this.addCustomFields(); - - }, - - onLoad: function () { - - // Enable edit button according to permission level. - this.getTopToolbar().getComponent("edit").setDisabled(this.data.permissionLevel < go.permissionLevels.write); - this.deleteItem.setDisabled(this.data.permissionLevel < go.permissionLevels.writeAndDelete); - - go.modules.tutorial.music.ArtistDetail.superclass.onLoad.call(this); - }, - - initToolbar: function () { - - var items = this.tbar || []; - - items = items.concat([ - '->', - { - itemId: "edit", - iconCls: 'ic-edit', - tooltip: t("Edit"), - handler: function (btn, e) { - var dlg = new go.modules.tutorial.music.ArtistDialog(); - dlg.show(); - dlg.load(this.data.id); - }, - scope: this - }, - - { - - iconCls: 'ic-more-vert', - menu: [ - { - iconCls: "ic-print", - text: t("Print"), - handler: function () { - this.body.print({title: this.data.name}); - }, - scope: this - }, - '-', - this.deleteItem = new Ext.menu.Item({ - itemId: "delete", - iconCls: 'ic-delete', - text: t("Delete"), - handler: function () { - Ext.MessageBox.confirm(t("Confirm delete"), t("Are you sure you want to delete this item?"), function (btn) { - if (btn != "yes") { - return; - } - this.entityStore.set({destroy: [this.currentId]}); - }, this); - }, - scope: this - }) - - ] - }]); - - var tbarCfg = { - disabled: true, - items: items - }; - - return new Ext.Toolbar(tbarCfg); - } - -}); \ No newline at end of file diff --git a/music/views/extjs3/ArtistDialog.js b/music/views/extjs3/ArtistDialog.js deleted file mode 100644 index 4697908..0000000 --- a/music/views/extjs3/ArtistDialog.js +++ /dev/null @@ -1,80 +0,0 @@ -go.modules.tutorial.music.ArtistDialog = Ext.extend(go.form.Dialog, { - // Change to true to remember state - stateful: false, - stateId: 'music-aritst-dialog', - title: t('Artist'), - - //The dialog set's entities in an go.data.EntityStore. This store notifies all - //connected go.data.Store view stores to update. - entityStore: "Artist", - autoHeight: true, - - // return an array of form items here. - initFormItems: function () { - return [{ - // it's recommended to wrap all fields in field sets for consistent style. - xtype: 'fieldset', - title: t("Artist information"), - items: [ - this.avatarComp = new go.form.ImageField({ - name: 'photo' - }), - - { - xtype: 'textfield', - name: 'name', - fieldLabel: t("Name"), - anchor: '100%', - allowBlank: false - }] - }, - - { - xtype: "fieldset", - title: t("Albums"), - - items: [ - { - //For relational properties we can use the "go.form.FormGroup" component. - //It's a sub form for the "albums" array property. - - xtype: "formgroup", - name: "albums", - hideLabel: true, - mapKey: 'id', - - // this will add dp(16) padding between rows. - pad: true, - - //the itemCfg is used to create a component for each "album" in the array. - itemCfg: { - layout: "form", - defaults: { - anchor: "100%" - }, - items: [{ - xtype: "hidden", - name: "id" - }, { - xtype: "textfield", - fieldLabel: t("Name"), - name: "name" - }, - - { - xtype: "datefield", - fieldLabel: t("Release date"), - name: "releaseDate" - }, - - { - xtype: "genrecombo" - } - ] - } - } - ] - } - ]; - } -}); \ No newline at end of file diff --git a/music/views/extjs3/ArtistGrid.js b/music/views/extjs3/ArtistGrid.js deleted file mode 100644 index d18acb5..0000000 --- a/music/views/extjs3/ArtistGrid.js +++ /dev/null @@ -1,118 +0,0 @@ -go.modules.tutorial.music.ArtistGrid = Ext.extend(go.grid.GridPanel, { - initComponent: function () { - - // Use a Group Office store that is connected with an go.data.EntityStore for automatic updates. - this.store = new go.data.Store({ - fields: [ - 'id', - 'name', - 'photo', //This is a blob id. A download URL can be retreived with go.Jmap.downloadUrl(record.data.photo) - - {name: 'createdAt', type: 'date'}, - {name: 'modifiedAt', type: 'date'}, - - // You can use "relation" as a store data type. This will autmatically - // fetch the related entity by the definition in Module.js. - {name: 'creator', type: "relation"}, - {name: 'modifier', type: "relation"}, - - // Every entity has permission levels. go.permissionLevels.read, write, - // writeAndDelete and manage - 'permissionLevel', - 'albumCount' - ], - - // The connected entity store. When Artists are changed the store will - // update automatically - entityStore: "Artist" - }); - - Ext.apply(this, { - - columns: [ - { - id: 'id', - hidden: true, - header: 'ID', - width: dp(40), - sortable: true, - dataIndex: 'id' - }, - { - id: 'name', - header: t('Name'), - width: dp(75), - sortable: true, - dataIndex: 'name', - renderer: function (value, metaData, record, rowIndex, colIndex, store) { - - //Render an avatar for the artist. - var style = record.data.photo ? 'background-image: url(' + go.Jmap.downloadUrl(record.data.photo) + ')"' : ''; - - return '
\ -
\ -
' + record.get('name') + '
\ -
'; - } - }, - { - id: 'albumcount', - sortable: false, - header: t('album_count','music','tutorial'), - dataIndex: 'albumCount', - width: dp(80) - }, - { - xtype: "datecolumn", - id: 'createdAt', - header: t('Created at'), - width: dp(160), - sortable: true, - dataIndex: 'createdAt', - hidden: true - }, - { - xtype: "datecolumn", - hidden: false, - id: 'modifiedAt', - header: t('Modified at'), - width: dp(160), - sortable: true, - dataIndex: 'modifiedAt' - }, - { - hidden: true, - header: t('Created by'), - width: dp(160), - sortable: true, - dataIndex: 'creator', - renderer: function (v) { - return v ? v.displayName : "-"; - } - }, - { - hidden: true, - header: t('Modified by'), - width: dp(160), - sortable: true, - dataIndex: 'modifier', - renderer: function (v) { - return v ? v.displayName : "-"; - } - } - ], - - viewConfig: { - emptyText: 'description

' + t("No items to display") + '

' - }, - - autoExpandColumn: 'name', - - // Change to true to remember grid state - stateful: false, - stateId: 'music-artist-grid' - }); - - go.modules.tutorial.music.ArtistGrid.superclass.initComponent.call(this); - } -}); \ No newline at end of file diff --git a/music/views/extjs3/GenreCombo.js b/music/views/extjs3/GenreCombo.js deleted file mode 100644 index 88eca83..0000000 --- a/music/views/extjs3/GenreCombo.js +++ /dev/null @@ -1,22 +0,0 @@ -go.modules.tutorial.music.GenreCombo = Ext.extend(go.form.ComboBox, { - fieldLabel: t("Genre"), - hiddenName: 'genreId', - anchor: '100%', - emptyText: t("Please select..."), - pageSize: 50, - valueField: 'id', - displayField: 'name', - triggerAction: 'all', - editable: true, - selectOnFocus: true, - forceSelection: true, - allowBlank: false, - store: { - xtype: "gostore", - fields: ['id', 'name'], - entityStore: "Genre" - } -}); - -// Register an xtype so we can use the component easily. -Ext.reg("genrecombo", go.modules.tutorial.music.GenreCombo); \ No newline at end of file diff --git a/music/views/extjs3/GenreFilter.js b/music/views/extjs3/GenreFilter.js deleted file mode 100644 index 2ccb378..0000000 --- a/music/views/extjs3/GenreFilter.js +++ /dev/null @@ -1,132 +0,0 @@ -go.modules.tutorial.music.GenreFilter = Ext.extend(go.grid.GridPanel, { - viewConfig: { - forceFit: true, - autoFill: true - }, - - initComponent: function () { - - // Row actions is a special grid column with an actions menu in it. - var actions = this.initRowActions(); - - // A selection model with checkboxes in this filter. - var selModel = new Ext.grid.CheckboxSelectionModel(); - - // A toolbar that consists out of two rows. - var tbar = { - xtype: "container", - items: [ - { - items: this.tbar || [], - xtype: 'toolbar' - }, - new Ext.Toolbar({ - items: [{xtype: "selectallcheckbox"}] - }) - ] - }; - - Ext.apply(this, { - - tbar: tbar, - - // We use a "go.data.Store" that connects with an Entity store. This store updates automatically when entities change. - store: new go.data.Store({ - fields: ['id', 'name', 'aclId', "permissionLevel"], - entityStore: "Genre" - }), - selModel: selModel, - plugins: [actions], - columns: [ - // The checkbox selection model must be added as a column too - selModel, - { - id: 'name', - header: t('Name'), - sortable: false, - dataIndex: 'name', - hideable: false, - draggable: false, - menuDisabled: true - }, - // The actions column showing a menu with delete and edit items. - actions - ], - - // Change to true to remember the state of the panel - stateful: false, - stateId: 'music-genre-filter' - }); - - go.modules.tutorial.music.GenreFilter.superclass.initComponent.call(this); - }, - - initRowActions: function () { - - var actions = new Ext.ux.grid.RowActions({ - menuDisabled: true, - hideable: false, - draggable: false, - fixed: true, - header: '', - hideMode: 'display', - keepSelection: true, - - actions: [{ - iconCls: 'ic-more-vert' - }] - }); - - actions.on({ - action: function (grid, record, action, row, col, e, target) { - this.showMoreMenu(record, e); - }, - scope: this - }); - - return actions; - - }, - - - showMoreMenu: function (record, e) { - if (!this.moreMenu) { - this.moreMenu = new Ext.menu.Menu({ - items: [ - { - itemId: "edit", - - // We use Material design icons. Look them up at https://material.io/tools/icons/?style=baseline. You can use ic-{name} as class names. - iconCls: 'ic-edit', - text: t("Edit"), - handler: function () { - var dlg = new go.modules.tutorial.music.GenreForm(); - dlg.load(this.moreMenu.record.id).show(); - }, - scope: this - }, { - itemId: "delete", - iconCls: 'ic-delete', - text: t("Delete"), - handler: function () { - Ext.MessageBox.confirm(t("Confirm delete"), t("Are you sure you want to delete this item?"), function (btn) { - if (btn != "yes") { - return; - } - go.Stores.get("Genre").set({destroy: [this.moreMenu.record.id]}); - }, this); - }, - scope: this - } - ] - }) - } - - this.moreMenu.getComponent("edit").setDisabled(record.get("permissionLevel") < GO.permissionLevels.manage); - this.moreMenu.getComponent("delete").setDisabled(record.get("permissionLevel") < GO.permissionLevels.manage); - - this.moreMenu.record = record; - - this.moreMenu.showAt(e.getXY()); - } -}); \ No newline at end of file diff --git a/music/views/extjs3/MainPanel.js b/music/views/extjs3/MainPanel.js deleted file mode 100644 index 08739fb..0000000 --- a/music/views/extjs3/MainPanel.js +++ /dev/null @@ -1,233 +0,0 @@ -go.modules.tutorial.music.MainPanel = Ext.extend(go.modules.ModulePanel, { - - // Use a responsive layout - layout: "responsive", - - // change responsive mode on 1000 pixels - layoutConfig: { - triggerWidth: 1000 - }, - - initComponent: function () { - - this.createArtistGrid(); - - // Every entity automatically configures a route. Route to the entity when selecting it in the grid. - this.artistGrid.on('navigate', function (grid, rowIndex, record) { - go.Router.goto("artist/" + record.id); - }, this); - - this.sidePanel = new Ext.Panel({ - layout: 'anchor', - defaults: { - anchor: '100%' - }, - width: dp(300), - cls: 'go-sidenav', - region: "west", - split: true, - autoScroll: true, - items: [ - this.createGenreFilter(), - this.createFilterPanel(), - ] - }); - // Create artist detail component - this.artistDetail = new go.modules.tutorial.music.ArtistDetail({ - region: "center", - tbar: [ - //add a back button for small screens - { - // this class will hide the button on large screens - cls: 'go-narrow', - iconCls: "ic-arrow-back", - handler: function () { - this.westPanel.show(); - }, - scope: this - }] - }); - - //Wrap the grids into another panel with responsive layout for the 3 column responsive layout to work. - this.westPanel = new Ext.Panel({ - region: "west", - layout: "responsive", - stateId: "go-music-west", - split: true, - width: dp(800), - narrowWidth: dp(500), //this will only work for panels inside another panel with layout=responsive. Not ideal but at the moment the only way I could make it work - items: [ - this.artistGrid, //first item is shown as default in narrow mode. - this.sidePanel - ] - }); - - //add the components to the main panel's items. - this.items = [ - this.westPanel, //first is default in narrow mode - this.artistDetail - ]; - - // Call the parent class' initComponent - go.modules.tutorial.music.MainPanel.superclass.initComponent.call(this); - - //Attach lister to changes of the filter selection. - //add buffer because it clears selection first and this would cause it to fire twice - this.genreFilter.getSelectionModel().on('selectionchange', this.onGenreFilterChange, this, {buffer: 1}); - - // Attach listener for running the module - this.on("afterrender", this.runModule, this); - }, - - - createArtistGrid: function() { - this.artistGrid = new go.modules.tutorial.music.ArtistGrid({ - region: "center", - - tbar: [ - //add a hamburger button for smaller screens - { - //this class will hide the button on large screens - cls: 'go-narrow', - iconCls: "ic-menu", - handler: function () { - this.genreFilter.show(); - }, - scope: this - }, - '->', - { - xtype: 'tbsearch' - }, - - // add button for creating new artists - this.addButton = new Ext.Button({ - iconCls: 'ic-add', - tooltip: t('Add'), - handler: function (btn) { - var dlg = new go.modules.tutorial.music.ArtistDialog({ - formValues: { - // you can pass form values like this - } - }); - dlg.show(); - }, - scope: this - }), - { - iconCls: 'ic-more-vert', - tooltip: t("More options"), - menu: [ - { - itemId: "delete", - iconCls: 'ic-delete', - text: t("Delete"), - handler: function () { - this.artistGrid.deleteSelected(); - }, - scope: this - } - ] - } - ], - - listeners: { - rowdblclick: this.onGridDblClick, - scope: this - } - }); - return this.artistGrid; - }, - - // Fired when the Genre filter selection changes - onGenreFilterChange: function (sm) { - - var selectedRecords = sm.getSelections(), - ids = selectedRecords.column('id'); //column is a special GO method that get's all the id's from the records in an array. - - this.artistGrid.store.setFilter('genres', {genres: ids}); - this.artistGrid.store.load(); - }, - - createGenreFilter: function() { - this.genreFilter = new go.modules.tutorial.music.GenreFilter({ - region: "west", - width: dp(300), - autoHeight: true, - - //render a split bar for resizing - split: true, - - tbar: [{ - xtype: "tbtitle", - text: t("Genres") - }, - '->', - - //add back button for smaller screens - { - //this class will hide it on larger screens - cls: 'go-narrow', - iconCls: "ic-arrow-forward", - tooltip: t("Artists"), - handler: function () { - this.artistGrid.show(); - }, - scope: this - } - ] - }); - return this.genreFilter; - }, - - createFilterPanel: function() { - return new Ext.Panel({ - autoHeight: true, - tbar: [ - { - xtype: 'tbtitle', - text: t("Filters") - }, - '->', - { - xtype: "button", - iconCls: "ic-add", - handler: function() { - var dlg = new go.filter.FilterDialog({ - entity: "Artist" - }); - dlg.show(); - }, - scope: this - } - ], - items: [ - this.filterGrid = new go.filter.FilterGrid({ - filterStore: this.artistGrid.store, - entity: "Artist" - }) - ] - }); - }, - - // Fired when the module panel is rendered. - runModule: function () { - // when this panel renders, load the genres and artists. - this.genreFilter.store.load(); - this.artistGrid.store.load(); - }, - - // Fires when an artist is double clicked in the grid. - onGridDblClick: function (grid, rowIndex, e) { - - //check permissions - var record = grid.getStore().getAt(rowIndex); - if (record.get('permissionLevel') < GO.permissionLevels.write) { - return; - } - - // Show dialog - var dlg = new go.modules.tutorial.music.ArtistDialog(); - dlg.load(record.id).show(); - } -}); \ No newline at end of file diff --git a/music/views/extjs3/Module.js b/music/views/extjs3/Module.js deleted file mode 100644 index dfd8781..0000000 --- a/music/views/extjs3/Module.js +++ /dev/null @@ -1,36 +0,0 @@ -go.Modules.register("tutorial", "music", { - mainPanel: "go.modules.tutorial.music.MainPanel", - - //The title is shown in the menu and tab bar - title: t("Music"), - - //All module entities must be defined here. Stores will be created for them. - entities: [ - "Genre", - { - name: "Artist", - relations: { - creator: {store: "User", fk: "createdBy"}, - modifier: {store: "User", fk: "createdBy"}, - - // 'albums' is a property of artist and has nested relations. - albums: { - type: go.Relations.TYPE_MAP, - genre: {store: "Genre", fk: "genreId"} - } - } - }, - { - name: "Review", - relations: { - creator: {store: "User", fk:"createdBy"}, - modifier: {store: "User", fk: "modifiedBy"} - } - } - ], - - //Put code to initialize the module here after the user is authenticated - //and has access to the module. - initModule: function () { - } -}); \ No newline at end of file diff --git a/music/views/extjs3/ReviewDialog.js b/music/views/extjs3/ReviewDialog.js deleted file mode 100644 index a7a787b..0000000 --- a/music/views/extjs3/ReviewDialog.js +++ /dev/null @@ -1,61 +0,0 @@ -go.modules.tutorial.music.ReviewDialog = Ext.extend(go.form.Dialog, { - stateId: 'album-review', - title: t("Review"), - entityStore: "Review", - width: dp(800), - height: dp(600), - maximizable: false, - collapsible: false, - modal: true, - - initFormItems: function () { - - this.addPanel(new go.permissions.SharePanel()); - - var items = [{ - xtype: 'fieldset', - anchor: "100% 100%", - items: [{ - xtype: 'textfield', - name: 'title', - fieldLabel: t("Title"), - anchor: '100%', - allowBlank: false - }, - { - xtype: 'radiogroup', - fieldLabel: t("Rating"), - name: "rating", - value: null, - items: [ - {boxLabel: t("It stinks"), inputValue: 1}, - {boxLabel: t("Meh"), inputValue: 2}, - {boxLabel: t("It's OK"), inputValue: 3}, - {boxLabel: t("It's pretty good"), inputValue: 4}, - {boxLabel: t("A stroke of genius"), inputValue: 5} - ] - }, - { - xtype: 'xhtmleditor', - name: 'body', - fieldLabel: "", - hideLabel: true, - anchor: '0 -90', - allowBlank: false, - listeners: { - scope: this, - ctrlenter: function() { - this.submit(); - } - } - }] - } - ]; - - return items; - }, - - onLoad : function(entityValues) { - this.supr().onLoad.call(this, entityValues); - } -}); diff --git a/music/views/extjs3/ReviewsModal.js b/music/views/extjs3/ReviewsModal.js deleted file mode 100644 index ad96658..0000000 --- a/music/views/extjs3/ReviewsModal.js +++ /dev/null @@ -1,197 +0,0 @@ -go.modules.tutorial.music.ReviewsModal = Ext.extend(go.Window, { - stateId: 'album-reviews', - title: t("Reviews"), - width: dp(1000), - height: dp(800), - maximizable: true, - collapsible: false, - modal: true, - stateful: true, - layout: 'fit', - initComponent: function () { - this.tools = [{ - id: "add", - handler: function () { - var dlg = new go.modules.tutorial.music.ReviewDialog(); - dlg.setValues({albumId: this.albumid}); - dlg.show(); - } - }]; - - this.store = new go.data.Store({ - fields: [ - 'id', - 'title', - 'body', - 'rating', - 'albumtitle', - 'createdBy', - {name: 'creator', type: "relation"}, - 'albumId', 'aclId', "permissionLevel" - ], - entityStore: "Review" - }); - - // Use a Group Office store that is connected with an go.data.EntityStore for automatic updates. - this.store.on('load', function (store, records, options) { - this.updateView(); - this.updateTitle(); - this.toggleAddBtn(); - }, this); - - this.store.on('remove', function () { - this.updateView(); - this.toggleAddBtn(); - }, this); - - this.on('destroy', function () { - this.store.destroy(); - }, this); - - this.on("expand", function () { - this.updateView(); - }, this); - - // Add a simple context menu. Make sure that the correct permissions are set - this.contextMenu = new Ext.menu.Menu({ - items: [{ - iconCls: 'ic-delete', - text: t("Delete"), - handler: function () { - - Ext.MessageBox.confirm(t("Confirm delete"), t("Are you sure you want to delete this item?"), function (btn) { - if (btn !== "yes") { - return; - } - go.Db.store("Review").set({destroy: [this.contextMenu.record.id]}); - }, this); - - }, - scope: this - }, { - iconCls: 'ic-edit', - text: t("Edit"), - handler: function () { - var dlg = new go.modules.tutorial.music.ReviewDialog(); - dlg.load(this.contextMenu.record.id).show(); - }, - scope: this - }] - }); - - var cntrClass = Ext.extend(Ext.Container, { - initComponent: function () { - Ext.Container.superclass.initComponent.call(this); - Ext.applyIf(this, go.panels.ScrollLoader); - this.initScrollLoader(); - }, - store: this.store, - scrollUp: true - }); - - this.items = [ - this.commentsContainer = new cntrClass({ - region: 'center', - autoScroll: true - }) - ]; - - go.modules.tutorial.music.ReviewsModal.superclass.initComponent.call(this); - }, - - updateView: function () { - this.commentsContainer.removeAll(); - this.store.each(function (r) { - var mineCls = r.get("createdBy") == go.User.id ? 'mine' : ''; - var readMore = new go.detail.ReadMore({ - cls: mineCls - }); - var creator = r.get("creator"); - if (!creator) { - creator = { - displayName: t("Unknown user") - }; - } - var avatar = { - xtype: 'box', - autoEl: { - tag: 'span', 'ext:qtip': t('{author} wrote: ') - .replace('{author}', creator.displayName) - }, - cls: 'photo ' + mineCls - }; - if (creator.avatarId) { - avatar.style = 'background-image: url(' + go.Jmap.thumbUrl(creator.avatarId, { - w: 40, - h: 40, - zc: 1 - }) + ');background-color: transparent;'; - } else { - avatar.html = go.util.initials(creator.displayName); - avatar.style = 'background-image: none'; - } - readMore.setText(this.getReviewText(r)); - - this.commentsContainer.add({ - xtype: "container", - cls: 'go-messages', - items: [{ - xtype: 'container', - label: t("Creator"), - items: [avatar, readMore] - }] - }); - // Add a context menu, make permissions dependent on ACL - readMore.on('render', function (me) { - me.getEl().on("contextmenu", function (e, target, obj) { - e.stopEvent(); - - if (r.data.permissionLevel >= go.permissionLevels.write) { - this.contextMenu.record = r; - this.contextMenu.showAt(e.xy); - } - - }, this); - }, this); - }, this); - - this.doLayout(); - var height = 7; // padding on composer - this.commentsContainer.items.each(function (item, i) { - height += item.getOuterSize().height; - }); - }, - - // Update window title by adding the album title - updateTitle: function () { - var r = this.store.getAt(0), title = this.title; - if (typeof (r) !== "undefined") { - this.setTitle(t("Reviews")+" " + t('for') + " " + - Ext.util.Format.htmlEncode(r.get('albumtitle'))); - } else { - this.setTitle(t("Reviews")); - } - }, - - // Check whether current user had added a review. If they have, hide the add button. - toggleAddBtn: function () { - if (this.store.query('createdBy', go.User.id).getCount() > 0) { - this.tools.add.hide(); - } else { - var r = this.store.getAt(0); - if (typeof (r) !== "undefined") { - this.tools.add.albumid = r.get("albumId"); - } - } - }, - - // Render the review text in a nice fashion - getReviewText: function (r) { - var s = "

" + r.get("title") + "

"; - for (var ii = 1; ii <= 5; ii++) { - s += "star" + (r.get('rating') < ii ? "_border" : "") + ""; - } - s += "

" + Ext.util.Format.htmlDecode(r.get('body')) + "

"; - return s; - } -}); diff --git a/music/views/extjs3/scripts.txt b/music/views/extjs3/scripts.txt deleted file mode 100644 index 1efd2b0..0000000 --- a/music/views/extjs3/scripts.txt +++ /dev/null @@ -1,9 +0,0 @@ -Module.js -MainPanel.js -GenreFilter.js -ArtistGrid.js -GenreCombo.js -ArtistDialog.js -ArtistDetail.js -ReviewDialog.js -ReviewsModal.js \ No newline at end of file diff --git a/music/views/extjs3/themes/default/src/style.scss b/music/views/extjs3/themes/default/src/style.scss deleted file mode 100644 index aaa3e59..0000000 --- a/music/views/extjs3/themes/default/src/style.scss +++ /dev/null @@ -1,7 +0,0 @@ -.go-detail-view-avatar { - text-align:center; - & > .avatar { - width: 120px; - height: 120px; - } -} \ No newline at end of file diff --git a/music/views/extjs3/themes/default/style.css b/music/views/extjs3/themes/default/style.css deleted file mode 100644 index d766e1c..0000000 --- a/music/views/extjs3/themes/default/style.css +++ /dev/null @@ -1,9 +0,0 @@ -.go-detail-view-avatar { - text-align: center; -} -.go-detail-view-avatar > .avatar { - width: 120px; - height: 120px; -} - -/*# sourceMappingURL=style.css.map */ diff --git a/music/views/extjs3/themes/default/style.css.map b/music/views/extjs3/themes/default/style.css.map deleted file mode 100644 index 47889f2..0000000 --- a/music/views/extjs3/themes/default/style.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sourceRoot":"","sources":["src/style.scss"],"names":[],"mappings":"AAAA;EACE;;AACA;EACE;EACA","file":"style.css"} \ No newline at end of file diff --git a/music/views/goui/script/Artist.ts b/music/views/goui/script/Artist.ts index 2091d10..9eff742 100644 --- a/music/views/goui/script/Artist.ts +++ b/music/views/goui/script/Artist.ts @@ -13,7 +13,7 @@ export interface Artist extends BaseEntity { export interface Album { name: string, - releaseDate: DateTime, + releaseDate: DateTime|string, genreId: EntityID, artistId: EntityID, id: EntityID diff --git a/music/views/goui/script/ArtistDetail.ts b/music/views/goui/script/ArtistDetail.ts index b19d417..6dc62b8 100644 --- a/music/views/goui/script/ArtistDetail.ts +++ b/music/views/goui/script/ArtistDetail.ts @@ -1,5 +1,4 @@ import { - Button, Component, DataSourceForm, avatar, @@ -15,13 +14,13 @@ import { Table, tbar, menu, - hr, - displayfield + displayfield, + DateTime } from "@intermesh/goui"; import {DetailPanel, img, jmapds, router} from "@intermesh/groupoffice-core"; import {ArtistWindow} from "./ArtistWindow.js"; import {AlbumWindow} from "./AlbumWindow.js"; -import {Artist} from "./Artist.js"; +import {Album, Artist} from "./Artist.js"; export class ArtistDetail extends DetailPanel { private form: DataSourceForm; @@ -96,12 +95,12 @@ export class ArtistDetail extends DetailPanel { id: "name", header: t("Title"), resizable: true, - sortable: true + sortable: false }), datecolumn({ id: "releaseDate", header: t("Release date"), - sortable: true + sortable: false }), column({ resizable: true, @@ -127,9 +126,10 @@ export class ArtistDetail extends DetailPanel { dlg.load(album); dlg.show(); } - }), hr(), btn({ + }), btn({ icon: "delete", text: t("Delete"), handler: async (btn) => { - // TODO... + const a = this.entity!.albums.filter(album => album.id !== record.id); + jmapds("Artist").update(this.entity!.id, {albums: a}); } })) }) @@ -140,6 +140,12 @@ export class ArtistDetail extends DetailPanel { ) ); + this.addCustomFields(); + // this.addComments(); + // this.addFiles(); + // this.addLinks(); + // this.addHistory(); + this.toolbar.items.add( btn({ icon: "edit", @@ -172,7 +178,12 @@ export class ArtistDetail extends DetailPanel { } else { pnl.avatarContainer.items.replace(avatar({cls: "goui-avatar", displayName: entity.name})); } + entity.albums.sort((a: Album, b: Album) => { + const ra: string = a.releaseDate, rb: string = b.releaseDate; + + return DateTime.createFromFormat(ra, "Y-m-d")!.compare(DateTime.createFromFormat(rb, "Y-m-d")!); + }); this.albumsTable.store.loadData(entity.albums, false); - }) + }); } } \ No newline at end of file diff --git a/music/views/goui/style/style.scss b/music/views/goui/style/style.scss index 6958a5c..f764c1e 100644 --- a/music/views/goui/style/style.scss +++ b/music/views/goui/style/style.scss @@ -1,6 +1,5 @@ .go-detail-view-avatar { min-width: 240px; - //min-height: 240px; text-align: center; display: flex; justify-content: center; From 30f0c394c55d0e06a6eb743c20b3a3990f099b66 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Tue, 26 Nov 2024 16:56:44 +0100 Subject: [PATCH 14/17] First full working version of GOUI module --- music/model/Album.php | 23 ++++---- music/model/Review.php | 2 +- music/views/goui/script/Artist.ts | 11 ++++ music/views/goui/script/ArtistDetail.ts | 69 ++++++++++++++++------ music/views/goui/script/Index.ts | 5 ++ music/views/goui/script/ReviewWindow.ts | 74 ++++++++++++++++++++++++ music/views/goui/script/ReviewsWindow.ts | 67 +++++++++++++++++++++ music/views/goui/style/style.scss | 3 +- 8 files changed, 224 insertions(+), 30 deletions(-) create mode 100644 music/views/goui/script/ReviewWindow.ts create mode 100644 music/views/goui/script/ReviewsWindow.ts diff --git a/music/model/Album.php b/music/model/Album.php index bd3b247..0e413f7 100644 --- a/music/model/Album.php +++ b/music/model/Album.php @@ -1,6 +1,7 @@ * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 */ +final class Album extends Property +{ -final class Album extends Property { - /** - * + * * @var int - */ + */ public int $id; - /** @var int */ + /** @var int */ public int $artistId; - /** @var string */ + /** @var string */ public string $name; - /** @var DateTime */ + /** @var DateTime */ public DateTime $releaseDate; - /** @var int */ + /** @var int */ public int $genreId; public array $reviews; - protected static function defineMapping(): Mapping { + protected static function defineMapping(): Mapping + { return parent::defineMapping() ->addTable("tutorial_music_album", "album") ->addScalar('reviews', 'tutorial_music_review', ['id' => 'albumId']); } + } \ No newline at end of file diff --git a/music/model/Review.php b/music/model/Review.php index b14faab..159e016 100644 --- a/music/model/Review.php +++ b/music/model/Review.php @@ -36,7 +36,7 @@ protected static function defineMapping(): Mapping return parent::defineMapping() ->addTable('tutorial_music_review') ->addQuery((new Query())->select('a.name AS albumtitle') - ->join('tutorial_music_album', 'a', 'a.id=music_review.albumId')); + ->join('tutorial_music_album', 'a', 'a.id=tutorial_music_review.albumId')); } protected function internalSave(): bool diff --git a/music/views/goui/script/Artist.ts b/music/views/goui/script/Artist.ts index 9eff742..509e67b 100644 --- a/music/views/goui/script/Artist.ts +++ b/music/views/goui/script/Artist.ts @@ -17,4 +17,15 @@ export interface Album { genreId: EntityID, artistId: EntityID, id: EntityID + reviews: EntityID[] +} + +export interface Review extends BaseEntity { + albumId: EntityID, + aclId: EntityID, + createdBy: EntityID, + modifiedBy: EntityID, + rating: 1|2|3|4|5, + title: string, + body: string } \ No newline at end of file diff --git a/music/views/goui/script/ArtistDetail.ts b/music/views/goui/script/ArtistDetail.ts index 6dc62b8..7aa70d9 100644 --- a/music/views/goui/script/ArtistDetail.ts +++ b/music/views/goui/script/ArtistDetail.ts @@ -15,12 +15,14 @@ import { tbar, menu, displayfield, - DateTime + DateTime } from "@intermesh/goui"; -import {DetailPanel, img, jmapds, router} from "@intermesh/groupoffice-core"; +import {client, DetailPanel, img, jmapds, router} from "@intermesh/groupoffice-core"; import {ArtistWindow} from "./ArtistWindow.js"; import {AlbumWindow} from "./AlbumWindow.js"; -import {Album, Artist} from "./Artist.js"; +import {Album, Artist, Review} from "./Artist.js"; +import {ReviewsWindow} from "./ReviewsWindow"; +import {ReviewWindow} from "./ReviewWindow"; export class ArtistDetail extends DetailPanel { private form: DataSourceForm; @@ -116,22 +118,53 @@ export class ArtistDetail extends DetailPanel { // sticky: true, width: 32, id: "btn", - renderer: (columnValue: any, record, td, table, rowIndex) => { - + renderer: async (columnValue: any, record, td, table, rowIndex) => { + const user = await client.getUser(); + let hasReviewed = false, reviewId = undefined; + for(const currId of record.reviews) { + const curr = await jmapds("Review").single(currId); + if (curr!.createdBy == user!.id) { + hasReviewed = true; + reviewId = curr!.id; + break; + } + } return btn({ icon: "more_vert", menu: menu({}, btn({ - icon: "edit", text: t("Edit"), handler: async (_btn) => { - const dlg = new AlbumWindow(this.entity!); - const album = table.store.get(rowIndex)!; - dlg.load(album); - dlg.show(); - } - }), btn({ - icon: "delete", text: t("Delete"), handler: async (btn) => { - const a = this.entity!.albums.filter(album => album.id !== record.id); - jmapds("Artist").update(this.entity!.id, {albums: a}); - } - })) + icon: "edit", text: t("Edit"), handler: async (_btn) => { + const dlg = new AlbumWindow(this.entity!); + const album = table.store.get(rowIndex)!; + dlg.load(album); + dlg.show(); + } + }), + btn({ + icon: "delete", text: t("Delete"), handler: async (btn) => { + const a = this.entity!.albums.filter(album => album.id !== record.id); + jmapds("Artist").update(this.entity!.id, {albums: a}); + } + }), + btn({ + icon: "reviews", + text: t("Show reviews"), + hidden: !record.reviews.length, + handler: (btn) => { + const w = new ReviewsWindow(record); + w.show(); + } + }), + btn({ + icon: "rate_review", + text: hasReviewed ? t("Update review") : t("Write review"), + handler: (_btn) => { + const w = new ReviewWindow(record); + if (hasReviewed) { + w.load(reviewId!); + } + w.show(); + } + }), + ) }) } }) @@ -171,7 +204,7 @@ export class ArtistDetail extends DetailPanel { void this.form.load(entity.id); if (entity!.photo) { pnl.avatarContainer.items.replace(img({ - cls: "goui-avatar", + cls: "goui-avatar-detail", blobId: entity.photo, title: entity.name })); diff --git a/music/views/goui/script/Index.ts b/music/views/goui/script/Index.ts index d3b7a5d..0334337 100644 --- a/music/views/goui/script/Index.ts +++ b/music/views/goui/script/Index.ts @@ -5,6 +5,11 @@ import {t, translate, EntityID} from "@intermesh/goui"; modules.register( { package: "tutorial", name: "music", + entities: [ + "Genre", + "Artist", + "Review" + ], async init () { client.on("authenticated", (client, session) => { diff --git a/music/views/goui/script/ReviewWindow.ts b/music/views/goui/script/ReviewWindow.ts new file mode 100644 index 0000000..28a8cbd --- /dev/null +++ b/music/views/goui/script/ReviewWindow.ts @@ -0,0 +1,74 @@ +import { FormWindow, router } from "@intermesh/groupoffice-core"; +import {Album} from "./Artist"; +import {fieldset, numberfield, select, t, textarea, textfield } from "@intermesh/goui"; + +export class ReviewWindow extends FormWindow { + + private readonly data: Album; + constructor(data: Album) { + super("Review"); + this.data = data; + this.title = t("Review") + ": " + data.name; + + this.stateId = "add-review-dialog"; + this.maximizable = true; + this.resizable = true; + this.modal = true; + this.width = 640; + + this.form.on("save", (form, data, isNew) => { + router.goto("artist/" + this.data.artistId); + }); + + this.generalTab.items.add( + fieldset({}, + textfield({ + name: "title", + label: t("Title"), + required: true + }), + numberfield({ + hidden: true, + value: parseInt(this.data.id), + required: true, + name: "albumId" + }), + select({ + label: t("Rating"), + name: "rating", + required: true, + options: [ + { + value: 1, + name: t("1 star") + }, + { + value: 2, + name: t("2 stars") + }, + { + value: 3, + name: t("3 stars") + }, + { + value: 4, + name: t("4 stars") + }, + { + value: 5, + name: t("5 stars") + }, + + ] + }), + textarea({ + required: true, + name: "body", + label: t("Your review") + }) + ) + ) + + this.addSharePanel(); + } +} \ No newline at end of file diff --git a/music/views/goui/script/ReviewsWindow.ts b/music/views/goui/script/ReviewsWindow.ts new file mode 100644 index 0000000..df56521 --- /dev/null +++ b/music/views/goui/script/ReviewsWindow.ts @@ -0,0 +1,67 @@ +import {avatar, btn, comp, Component, h3, h4, t, tbar, Window} from "@intermesh/goui"; +import {Album} from "./Artist"; +import {img, jmapds} from "@intermesh/groupoffice-core"; + +export class ReviewsWindow extends Window { + + constructor(data: Album) { + super(); + this.title = `${t("Reviews")} ${t("for")}: ${data.name}`; + + this.stateId = "reviews-dialog"; + this.maximizable = true; + this.resizable = true; + this.modal = true; + this.width = 640; + this.height = 768; + this.cls = "vbox gap"; + + const scrollCmp = comp({cls: "scroll", flex: 1}); + + jmapds("Review").get(data.reviews).then(async (result) => { + for (const review of result.list) { + const user = await jmapds("User").single(review!.createdBy); + const avatarCnt = comp({ + cls: "go-detail-view-avatar pad", + itemId: "avatar-container" + }); + if(user!.avatarId) { + avatarCnt.items.replace( + img({ + cls: "goui-avatar", + blobId: user!.avatarId, + title: user!.displayName + }) + ); + } else { + avatarCnt.items.replace(avatar({cls: "goui-avatar", displayName: user!.name})); + } + + scrollCmp.items.add(comp({cls: "card pad"}, + comp({cls: "hbox",},avatarCnt, + comp({cls: "vbox"}, + h3(review!.title), + h4(t("By")+ " " +user!.displayName), + )), + this.addRating(review.rating), + comp({cls: "border-bottom", html: review!.body}) + )); + } + }).finally(() => { + this.items.add(scrollCmp, tbar({}, "->", btn({ + icon: "close", + text: t("Close"), + handler: () => this.close() + }))) + }); + + } + + private addRating(rating: 1 | 2 | 3 | 4 | 5): Component { + const cmp = comp({cls: "hbox"}); + for (let r = 1; r <= rating; r++) { + cmp.items.add(comp({cls: "icon", tagName: "i", text: "star"})) + } + return cmp; + } +} \ No newline at end of file diff --git a/music/views/goui/style/style.scss b/music/views/goui/style/style.scss index f764c1e..54601a9 100644 --- a/music/views/goui/style/style.scss +++ b/music/views/goui/style/style.scss @@ -3,8 +3,9 @@ text-align: center; display: flex; justify-content: center; - & > .goui-avatar { + & > .goui-avatar-detail { width: 120px !important; height: 120px !important; + border-radius: 50%; } } \ No newline at end of file From cde1f94e34407ca78a9efbb6e4081614737ac103 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Mon, 2 Dec 2024 16:45:57 +0100 Subject: [PATCH 15/17] port to 6.9 --- music/views/goui/package-lock.json | 653 +++++++++++------------ music/views/goui/package.json | 9 +- music/views/goui/script/ArtistDetail.ts | 202 +++---- music/views/goui/script/ReviewsWindow.ts | 2 +- music/views/goui/tsconfig.json | 2 +- 5 files changed, 415 insertions(+), 453 deletions(-) diff --git a/music/views/goui/package-lock.json b/music/views/goui/package-lock.json index 0504030..ce2a4e5 100644 --- a/music/views/goui/package-lock.json +++ b/music/views/goui/package-lock.json @@ -456,7 +456,7 @@ "@parcel/watcher-win32-x64": "2.5.0" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-android-arm64": { + "node_modules/@parcel/watcher-android-arm64": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", @@ -477,7 +477,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-darwin-arm64": { + "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", @@ -498,7 +498,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-darwin-x64": { + "node_modules/@parcel/watcher-darwin-x64": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", @@ -519,7 +519,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-freebsd-x64": { + "node_modules/@parcel/watcher-freebsd-x64": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", @@ -540,7 +540,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm-glibc": { + "node_modules/@parcel/watcher-linux-arm-glibc": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", @@ -561,7 +561,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm-musl": { + "node_modules/@parcel/watcher-linux-arm-musl": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", @@ -582,7 +582,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm64-glibc": { + "node_modules/@parcel/watcher-linux-arm64-glibc": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", @@ -603,7 +603,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-arm64-musl": { + "node_modules/@parcel/watcher-linux-arm64-musl": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", @@ -624,7 +624,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-x64-glibc": { + "node_modules/@parcel/watcher-linux-x64-glibc": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", @@ -645,7 +645,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-x64-musl": { + "node_modules/@parcel/watcher-linux-x64-musl": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", @@ -666,7 +666,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-win32-arm64": { + "node_modules/@parcel/watcher-win32-arm64": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", @@ -687,7 +687,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-win32-ia32": { + "node_modules/@parcel/watcher-win32-ia32": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", @@ -708,7 +708,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/@parcel/watcher-win32-x64": { + "node_modules/@parcel/watcher-win32-x64": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", @@ -729,133 +729,44 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" - }, "engines": { "node": ">=8" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/@parcel/watcher/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "to-regex-range": "^5.0.1" + "color-convert": "^2.0.1" }, "engines": { "node": ">=8" - } - }, - "node_modules/@parcel/watcher/node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@parcel/watcher/node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@parcel/watcher/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/@parcel/watcher/node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@parcel/watcher/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@parcel/watcher/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "is-number": "^7.0.0" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=8.0" + "node": ">=8" } }, "node_modules/chalk": { @@ -875,52 +786,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/chalk/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/chalk/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -950,20 +815,41 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/chokidar/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": ">= 14.16.0" + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=7.0.0" } }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/concurrently": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", @@ -990,6 +876,27 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", @@ -1030,256 +937,226 @@ "@esbuild/win32-x64": "0.24.0" } }, - "node_modules/immutable": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", - "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, "dependencies": { - "tslib": "^2.1.0" + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "0BSD" + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "node_modules/sass": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", - "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" + "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=8" } }, - "node_modules/supports-color/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", - "bin": { - "tree-kill": "cli.js" + "optional": true, + "engines": { + "node": ">=0.12.0" } }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } + "license": "MIT" }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=8.6" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true }, - "node_modules/yargs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, + "optional": true, "engines": { - "node": ">=8" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/yargs/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/yargs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=0.10.0" } }, - "node_modules/yargs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" } }, - "node_modules/yargs/node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/sass": { + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", + "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/yargs/node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/yargs/node_modules/string-width": { + "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", @@ -1294,7 +1171,7 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -1307,7 +1184,68 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/wrap-ansi": { + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", @@ -1325,7 +1263,7 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/yargs/node_modules/y18n": { + "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", @@ -1335,7 +1273,26 @@ "node": ">=10" } }, - "node_modules/yargs/node_modules/yargs-parser": { + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", diff --git a/music/views/goui/package.json b/music/views/goui/package.json index ea03982..b324252 100644 --- a/music/views/goui/package.json +++ b/music/views/goui/package.json @@ -9,11 +9,14 @@ "scripts": { "start": "concurrently --kill-others \"npm run start:ts\" \"npm run start:sass\"", - "start:ts": "npx esbuild script/Index.ts --external:../../../../../../views/goui/* --bundle --watch --sourcemap --format=esm --target=esnext --outdir=dist", + "start:ts": "node ../../../../../../views/goui/esbuild-module.mjs watch", "start:sass": "npx sass --watch style:dist", "build": "npm run build:sass && npm run build:ts", - "build:ts": "npx esbuild script/Index.ts --external:../../../../../../views/goui/* --bundle --minify --sourcemap --format=esm --target=esnext --outdir=dist", - "build:sass": "npx sass --style=compressed style:dist" + "build:ts": "node ../../../../../../views/goui/esbuild-module.mjs", + "build:sass": "npx sass --style=compressed style:dist", + "build:dts": "npx tsc --emitDeclarationOnly --declaration", + + "test": "npx tsc --noEmit" } } diff --git a/music/views/goui/script/ArtistDetail.ts b/music/views/goui/script/ArtistDetail.ts index 7aa70d9..a8ee352 100644 --- a/music/views/goui/script/ArtistDetail.ts +++ b/music/views/goui/script/ArtistDetail.ts @@ -15,7 +15,7 @@ import { tbar, menu, displayfield, - DateTime + DateTime, h4 } from "@intermesh/goui"; import {client, DetailPanel, img, jmapds, router} from "@intermesh/groupoffice-core"; import {ArtistWindow} from "./ArtistWindow.js"; @@ -72,112 +72,114 @@ export class ArtistDetail extends DetailPanel { ), ), - fieldset({legend: t("Albums")}, - tbar({}, "->", btn({ - icon: "add", cls: "primary", text: t("Add"), handler: () => { - const w = new AlbumWindow(this.entity!); - w.on("close", async () => { - this.load(this.entity!.id) - }); - w.show(); - } - })), - this.albumsTable = table({ - fitParent: true, - // headers: false, - store: store({ - data: [] - }), - columns: [ - column({ - id: "id", - hidden: true, + comp({cls:"card"}, + fieldset({}, + tbar({}, h4(t("Albums")), "->", btn({ + icon: "add", cls: "primary", text: t("Add"), handler: () => { + const w = new AlbumWindow(this.entity!); + w.on("close", async () => { + this.load(this.entity!.id) + }); + w.show(); + } + })), + this.albumsTable = table({ + fitParent: true, + // headers: false, + store: store({ + data: [] }), - column({ - id: "name", - header: t("Title"), - resizable: true, - sortable: false - }), - datecolumn({ - id: "releaseDate", - header: t("Release date"), - sortable: false - }), - column({ - resizable: true, - id: "genreId", - header: t("Genre"), - renderer: async (v) => { - const g = await jmapds("Genre").single(v); - return g!.name; - } - }), - column({ - resizable: false, - // sticky: true, - width: 32, - id: "btn", - renderer: async (columnValue: any, record, td, table, rowIndex) => { - const user = await client.getUser(); - let hasReviewed = false, reviewId = undefined; - for(const currId of record.reviews) { - const curr = await jmapds("Review").single(currId); - if (curr!.createdBy == user!.id) { - hasReviewed = true; - reviewId = curr!.id; - break; - } + columns: [ + column({ + id: "id", + hidden: true, + }), + column({ + id: "name", + header: t("Title"), + resizable: true, + sortable: false + }), + datecolumn({ + id: "releaseDate", + header: t("Release date"), + sortable: false + }), + column({ + resizable: true, + id: "genreId", + header: t("Genre"), + renderer: async (v) => { + const g = await jmapds("Genre").single(v); + return g!.name; } - return btn({ - icon: "more_vert", menu: menu({}, btn({ - icon: "edit", text: t("Edit"), handler: async (_btn) => { - const dlg = new AlbumWindow(this.entity!); - const album = table.store.get(rowIndex)!; - dlg.load(album); - dlg.show(); - } - }), - btn({ - icon: "delete", text: t("Delete"), handler: async (btn) => { - const a = this.entity!.albums.filter(album => album.id !== record.id); - jmapds("Artist").update(this.entity!.id, {albums: a}); - } - }), - btn({ - icon: "reviews", - text: t("Show reviews"), - hidden: !record.reviews.length, - handler: (btn) => { - const w = new ReviewsWindow(record); - w.show(); - } - }), - btn({ - icon: "rate_review", - text: hasReviewed ? t("Update review") : t("Write review"), - handler: (_btn) => { - const w = new ReviewWindow(record); - if (hasReviewed) { - w.load(reviewId!); + }), + column({ + resizable: false, + sticky: true, + width: 32, + id: "btn", + renderer: async (columnValue: any, record, td, table, rowIndex) => { + const user = client.user; + let hasReviewed = false, reviewId = undefined; + for(const currId of record.reviews) { + const curr = await jmapds("Review").single(currId); + if (curr!.createdBy == user!.id) { + hasReviewed = true; + reviewId = curr!.id; + break; + } + } + return btn({ + icon: "more_vert", menu: menu({}, btn({ + icon: "edit", text: t("Edit"), handler: async (_btn) => { + const dlg = new AlbumWindow(this.entity!); + const album = table.store.get(rowIndex)!; + dlg.load(album); + dlg.show(); } - w.show(); - } - }), - ) - }) - } - }) - ] - }) + }), + btn({ + icon: "delete", text: t("Delete"), handler: async (btn) => { + const a = this.entity!.albums.filter(album => album.id !== record.id); + jmapds("Artist").update(this.entity!.id, {albums: a}); + } + }), + btn({ + icon: "reviews", + text: t("Show reviews"), + hidden: !record.reviews.length, + handler: (btn) => { + const w = new ReviewsWindow(record); + w.show(); + } + }), + btn({ + icon: "rate_review", + text: hasReviewed ? t("Update review") : t("Write review"), + handler: (_btn) => { + const w = new ReviewWindow(record); + if (hasReviewed) { + w.load(reviewId!); + } + w.show(); + } + }), + ) + }) + } + }) + ] + }) + ) ) ); this.addCustomFields(); - // this.addComments(); - // this.addFiles(); - // this.addLinks(); - // this.addHistory(); + this.addComments(); + this.addFiles(); + this.addLinks(); + this.addHistory(); this.toolbar.items.add( btn({ diff --git a/music/views/goui/script/ReviewsWindow.ts b/music/views/goui/script/ReviewsWindow.ts index df56521..07f1f8b 100644 --- a/music/views/goui/script/ReviewsWindow.ts +++ b/music/views/goui/script/ReviewsWindow.ts @@ -34,7 +34,7 @@ export class ReviewsWindow extends Window { }) ); } else { - avatarCnt.items.replace(avatar({cls: "goui-avatar", displayName: user!.name})); + avatarCnt.items.replace(avatar({cls: "goui-avatar", displayName: user!.displayName})); } scrollCmp.items.add(comp({cls: "card pad"}, diff --git a/music/views/goui/tsconfig.json b/music/views/goui/tsconfig.json index 81a6670..70b097a 100644 --- a/music/views/goui/tsconfig.json +++ b/music/views/goui/tsconfig.json @@ -4,4 +4,4 @@ "baseUrl": ".", "outDir": "./dist" } -} \ No newline at end of file +} From 5399a263280d16a4ca6fcc79900180c923f24f0e Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Tue, 3 Dec 2024 09:36:21 +0100 Subject: [PATCH 16/17] Feedback MS / JA --- music/views/goui/script/ArtistWindow.ts | 2 +- music/views/goui/script/Index.ts | 2 +- music/views/goui/script/Main.ts | 203 +++++++++-------------- music/views/goui/script/ReviewsWindow.ts | 2 +- 4 files changed, 84 insertions(+), 125 deletions(-) diff --git a/music/views/goui/script/ArtistWindow.ts b/music/views/goui/script/ArtistWindow.ts index 5b848de..a583cca 100644 --- a/music/views/goui/script/ArtistWindow.ts +++ b/music/views/goui/script/ArtistWindow.ts @@ -14,7 +14,7 @@ export class ArtistWindow extends FormWindow { this.form.on("save", (form, data, isNew) => { if (isNew) { - router.goto("artist/" + data.id); + void router.goto("artist/" + data.id); } }) diff --git a/music/views/goui/script/Index.ts b/music/views/goui/script/Index.ts index 0334337..0b162ae 100644 --- a/music/views/goui/script/Index.ts +++ b/music/views/goui/script/Index.ts @@ -25,7 +25,7 @@ modules.register( { router.add(/^music\/(\d+)$/, (id: EntityID) => { modules.openMainPanel("music"); - mainPanel.load(id); + mainPanel.setArtistId(id); }); router.add(/^music$/, () => { diff --git a/music/views/goui/script/Main.ts b/music/views/goui/script/Main.ts index a7b4284..9a4de50 100644 --- a/music/views/goui/script/Main.ts +++ b/music/views/goui/script/Main.ts @@ -7,34 +7,28 @@ import { Notifier, paginator, searchbtn, - splitter, t, tbar } from "@intermesh/goui"; import { authManager, router, - FilterCondition, + MainThreeColumnPanel, } from "@intermesh/groupoffice-core"; import {ArtistTable} from "./ArtistTable.js"; import {GenreTable} from "./GenreTable.js"; import { ArtistDetail } from "./ArtistDetail.js"; import {ArtistWindow} from "./ArtistWindow.js"; -export class Main extends Component { - private artistTable: ArtistTable; - +export class Main extends MainThreeColumnPanel { + protected east!: ArtistDetail; + private artistTable!: ArtistTable; private genreTable!: GenreTable; - private west: Component; - private center: Component; - private east: ArtistDetail; constructor() { - super("section"); + super("music"); - this.id = "music"; - this.cls = "vbox fit"; this.on("render", async () => { try { await authManager.requireLogin(); @@ -44,119 +38,18 @@ export class Main extends Component { } await this.genreTable.store.load(); - await this.artistTable.store.load(); - }); - - - this.artistTable = new ArtistTable(); - this.artistTable.on("navigate", async (table: ArtistTable, rowIndex: number) => { - await router.goto("music/" + table.store.get(rowIndex)!.id); + await this.artistTable!.store.load(); }); - - this.west = this.createWest(); - this.items.add( - comp({ - flex: 1, cls: "hbox mobile-cards" - }, - - this.west, - - splitter({ - stateId: "music-splitter-west", - resizeComponentPredicate: this.west - }), - - this.center = comp({ - cls: 'active vbox', - itemId: 'table-container', - flex: 1, - style: { - minWidth: "365px", //for the resizer's boundaries - maxWidth: "850px" - } - }, - - tbar({}, - btn({ - cls: "for-small-device", - title: t("Menu"), - icon: "menu", - handler: (button, ev) => { - this.activatePanel(this.west); - } - }), - - '->', - - searchbtn({ - listeners: { - input: (sender, text) => { - - (this.artistTable.store.queryParams.filter as FilterCondition).text = text; - this.artistTable.store.load(); - - } - } - }), - - mstbar({table: this.artistTable}), - - btn({ - itemId: "add", - icon: "add", - cls: "filled primary", - handler: async () => { - const w = new ArtistWindow(); - w.on("close", async () => { - debugger; - }); - w.show(); - - } - }) - ), - - comp({ - flex: 1, - stateId: "music", - cls: "scroll border-top main" - }, - this.artistTable - ), - - - paginator({ - store: this.artistTable.store - }) - ), - - - splitter({ - stateId: "music-splitter", - resizeComponentPredicate: "table-container" - }), - - this.east = new ArtistDetail() - ) - ); } - private activatePanel(active: Component) { - this.center.el.classList.remove("active"); - this.east.el.classList.remove("active"); - this.west.el.classList.remove("active"); - - active.el.classList.add("active"); - } - - private createWest(): Component { + protected createWest(): Component { this.genreTable = new GenreTable(); this.genreTable.rowSelectionConfig = { multiSelect: true, listeners: { selectionchange: (tableRowSelect) => { const genreIds = tableRowSelect.selected.map((index: number) => tableRowSelect.list.store.get(index)!.id); - (this.artistTable.store.queryParams.filter as FilterCondition).genres = genreIds; + this.artistTable.store.queryParams.filter!.genres = genreIds; this.artistTable.store.load(); } } @@ -188,14 +81,80 @@ export class Main extends Component { ); } - async load(id?: EntityID) { - if(id) { - void this.east.load(id); - this.activatePanel(this.east); - } else { - this.activatePanel(this.center); - } + protected createEast(): ArtistDetail { + const detail = new ArtistDetail(); + detail.itemId = "detail"; + detail.stateId = "artist-detail"; + detail.toolbar.items.insert(0,this.showCenterButton()); + return detail; + } + + protected createCenter(): Component { + this.artistTable = new ArtistTable(); + this.artistTable.on("navigate", async (table: ArtistTable, rowIndex: number) => { + await router.goto("music/" + table.store.get(rowIndex)!.id); + }); + + return comp({ + cls: 'active vbox', + itemId: 'table-container', + flex: 1 + }, + + tbar({}, + btn({ + cls: "for-small-device", + title: t("Menu"), + icon: "menu", + handler: (button, ev) => { + this.activatePanel(this.west); + } + }), + + '->', + + searchbtn({ + listeners: { + input: (sender, text) => { + this.artistTable.store.queryParams.filter!.text = text; + this.artistTable.store.load(); + } + } + }), + + mstbar({table: this.artistTable}), + + btn({ + itemId: "add", + icon: "add", + cls: "filled primary", + handler: async () => { + const w = new ArtistWindow(); + w.on("close", async () => {}); + w.show(); + + } + }) + ), + + comp({ + flex: 1, + stateId: "music", + cls: "scroll border-top main" + }, + this.artistTable + ), + + + paginator({ + store: this.artistTable.store + }) + ); + } + async setArtistId(id: EntityID) { + void this.east.load(id); + this.activatePanel(this.east); } } diff --git a/music/views/goui/script/ReviewsWindow.ts b/music/views/goui/script/ReviewsWindow.ts index 07f1f8b..6b3983c 100644 --- a/music/views/goui/script/ReviewsWindow.ts +++ b/music/views/goui/script/ReviewsWindow.ts @@ -60,7 +60,7 @@ export class ReviewsWindow extends Window { private addRating(rating: 1 | 2 | 3 | 4 | 5): Component { const cmp = comp({cls: "hbox"}); for (let r = 1; r <= rating; r++) { - cmp.items.add(comp({cls: "icon", tagName: "i", text: "star"})) + cmp.items.add(comp({cls: "icon", tagName: "i", text: "star_rate"})) } return cmp; } From e1c8667c9e93fe5af73eb4d4b6f51dc28a362a30 Mon Sep 17 00:00:00 2001 From: Joachim van de Haterd Date: Tue, 7 Jan 2025 16:13:48 +0100 Subject: [PATCH 17/17] Ported to 6.9 / 25.X --- music/views/goui/script/ArtistDetail.ts | 8 ++++---- music/views/goui/script/ArtistTable.ts | 2 ++ music/views/goui/script/Main.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/music/views/goui/script/ArtistDetail.ts b/music/views/goui/script/ArtistDetail.ts index a8ee352..841ff64 100644 --- a/music/views/goui/script/ArtistDetail.ts +++ b/music/views/goui/script/ArtistDetail.ts @@ -176,10 +176,10 @@ export class ArtistDetail extends DetailPanel { ); this.addCustomFields(); - this.addComments(); - this.addFiles(); - this.addLinks(); - this.addHistory(); + // this.addComments(); + // this.addFiles(); + // this.addLinks(); + // this.addHistory(); this.toolbar.items.add( btn({ diff --git a/music/views/goui/script/ArtistTable.ts b/music/views/goui/script/ArtistTable.ts index c23765f..f139e6d 100644 --- a/music/views/goui/script/ArtistTable.ts +++ b/music/views/goui/script/ArtistTable.ts @@ -3,6 +3,8 @@ import { JmapDataSource, img, jmapds } from "@intermesh/groupoffice-core"; interface Artist extends BaseEntity { name: string, + active: boolean, + photo: string } export class ArtistTable extends Table { diff --git a/music/views/goui/script/Main.ts b/music/views/goui/script/Main.ts index 9a4de50..48ed0e8 100644 --- a/music/views/goui/script/Main.ts +++ b/music/views/goui/script/Main.ts @@ -83,7 +83,7 @@ export class Main extends MainThreeColumnPanel { protected createEast(): ArtistDetail { const detail = new ArtistDetail(); - detail.itemId = "detail"; + detail.itemId = "artistDetail"; detail.stateId = "artist-detail"; detail.toolbar.items.insert(0,this.showCenterButton()); return detail;