diff --git a/app/main/controller/admin.php b/app/main/controller/admin.php index fbfb6677..42da00e7 100644 --- a/app/main/controller/admin.php +++ b/app/main/controller/admin.php @@ -13,6 +13,7 @@ use Controller\Ccp\Sso; use Model\CharacterModel; use Model\CorporationModel; use lib\Config; +use Model\MapModel; class Admin extends Controller{ @@ -183,6 +184,23 @@ class Admin extends Controller{ $this->initMembers($f3, $character); break; + case 'maps': + switch($parts[1]){ + case 'active': + $objectId = (int)$parts[2]; + $value = (int)$parts[3]; + $this->activateMap($character, $objectId, $value); + + $f3->reroute('@admin(@*=/' . $parts[0] . ')'); + break; + case 'delete': + $objectId = (int)$parts[2]; + $this->deleteMap($character, $objectId); + $f3->reroute('@admin(@*=/' . $parts[0] . ')'); + break; + } + $this->initMaps($f3, $character); + break; case 'login': default: $f3->set('tplPage', 'login'); @@ -230,7 +248,6 @@ class Admin extends Controller{ */ protected function banCharacter(CharacterModel $character, $banCharacterId, $value){ $banCharacters = $this->filterValidCharacters($character, $banCharacterId); - foreach($banCharacters as $banCharacter){ $banCharacter->ban($value); $banCharacter->save(); @@ -248,7 +265,7 @@ class Admin extends Controller{ } /** - * checks whether a $character has admin access rights for $charcterId + * checks whether a $character has admin access rights for $characterId * -> must be in same corporation * @param CharacterModel $character * @param int $characterId @@ -270,6 +287,52 @@ class Admin extends Controller{ return $characters; } + /** + * @param CharacterModel $character + * @param int $mapId + * @param int $value + * @throws \Exception\PathfinderException + */ + protected function activateMap(CharacterModel $character, int $mapId, int $value){ + $maps = $this->filterValidMaps($character, $mapId); + foreach($maps as $map){ + $map->setActive((bool)$value); + $map->save($character); + } + } + + /** + * @param CharacterModel $character + * @param int $mapId + * @throws \Exception\PathfinderException + */ + protected function deleteMap(CharacterModel $character, int $mapId){ + $maps = $this->filterValidMaps($character, $mapId); + foreach($maps as $map){ + $map->erase(); + } + } + + /** + * checks whether a $character has admin access rights for $mapId + * @param CharacterModel $character + * @param int $mapId + * @return array + * @throws \Exception\PathfinderException + */ + protected function filterValidMaps(CharacterModel $character, int $mapId) : array { + $maps = []; + if($character->role === 'SUPERADMIN'){ + if($filterMaps = MapModel::getAll([$mapId])){ + $maps = $filterMaps; + } + }else{ + $maps = $character->getCorporation()->getMaps([$mapId], ['addInactive' => true, 'ignoreMapCount' => true]); + } + + return $maps; + } + /** * get log file for "admin" logs * @param string $type @@ -287,10 +350,8 @@ class Admin extends Controller{ */ protected function initMembers(\Base $f3, CharacterModel $character){ $data = (object) []; - if($characterCorporation = $character->getCorporation()){ $corporations = []; - switch($character->role){ case 'SUPERADMIN': if($accessCorporations = CorporationModel::getAll(['addNPC' => true])){ @@ -304,7 +365,7 @@ class Admin extends Controller{ foreach($corporations as $corporation){ if($characters = $corporation->getCharacters()){ - $data->corpMembers[$corporation->name] = $corporation->getCharacters(); + $data->corpMembers[$corporation->name] = $characters; } } @@ -317,4 +378,34 @@ class Admin extends Controller{ $f3->set('tplMembers', $data); } + /** + * init /maps page data + * @param \Base $f3 + * @param CharacterModel $character + */ + protected function initMaps(\Base $f3, CharacterModel $character){ + $data = (object) []; + if($characterCorporation = $character->getCorporation()){ + $corporations = []; + switch($character->role){ + case 'SUPERADMIN': + if($accessCorporations = CorporationModel::getAll(['addNPC' => true])){ + $corporations = $accessCorporations; + } + break; + case 'CORPORATION': + $corporations[] = $characterCorporation; + break; + } + + foreach($corporations as $corporation){ + if($maps = $corporation->getMaps([], ['addInactive' => true, 'ignoreMapCount' => true])){ + $data->corpMaps[$corporation->name] = $maps; + } + } + } + + $f3->set('tplMaps', $data); + } + } \ No newline at end of file diff --git a/app/main/controller/api/github.php b/app/main/controller/api/github.php index 974ed3da..8f76a013 100644 --- a/app/main/controller/api/github.php +++ b/app/main/controller/api/github.php @@ -52,6 +52,14 @@ class GitHub extends Controller\Controller { $apiResponse = \Web::instance()->request($apiPath, $options ); if($apiResponse['body']){ + $return = (object) []; + $return->releasesData = []; + $return->version = (object) []; + $return->version->current = Config::getPathfinderData('version'); + $return->version->last = ''; + $return->version->delta = null; + $return->version->dev = false; + // request succeeded -> format "Markdown" to "HTML" // result is JSON formed $releasesData = (array)json_decode($apiResponse['body']); @@ -62,7 +70,24 @@ class GitHub extends Controller\Controller { } $md = \Markdown::instance(); - foreach($releasesData as &$releaseData){ + foreach($releasesData as $key => &$releaseData){ + // check version ---------------------------------------------------------------------------------- + if($key === 0){ + $return->version->last = $releaseData->tag_name; + + if(version_compare( $return->version->current, $return->version->last, '>')){ + $return->version->dev = true; + } + } + + if( + !$return->version->dev && + version_compare( $releaseData->tag_name, $return->version->current, '>=') + ){ + $return->version->delta = ($key === count($releasesData) - 1) ? '>= ' . $key : $key; + } + + // format body ------------------------------------------------------------------------------------ if(isset($releaseData->body)){ $body = $releaseData->body; @@ -78,7 +103,10 @@ class GitHub extends Controller\Controller { $releaseData->body = $md->convert( trim($body) ); } } - $f3->set($cacheKey, $releasesData, $ttl); + + $return->releasesData = $releasesData; + + $f3->set($cacheKey, $return, $ttl); }else{ // request failed -> cache failed result (respect API request limit) $f3->set($cacheKey, false, 60 * 5); diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index fcd3b528..3b2e49af 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -207,6 +207,11 @@ class Map extends Controller\AccessController { 'status' => (bool)Config::getPathfinderData('slack.status') ]; + // Slack integration status ------------------------------------------------------------------------------- + $return->discord = [ + 'status' => (bool)Config::getPathfinderData('discord.status') + ]; + $f3->set(self::CACHE_KEY_INIT, $return, $expireTimeCache ); } diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php index 6349aa3c..c6ea1c8e 100644 --- a/app/main/controller/api/system.php +++ b/app/main/controller/api/system.php @@ -431,6 +431,7 @@ class System extends Controller\AccessController { $rallyData['pokeDesktop'] = $rallyData['pokeDesktop'] === '1'; $rallyData['pokeMail'] = $rallyData['pokeMail'] === '1'; $rallyData['pokeSlack'] = $rallyData['pokeSlack'] === '1'; + $rallyData['pokeDiscord'] = $rallyData['pokeDiscord'] === '1'; $rallyData['message'] = trim($rallyData['message']); $system->sendRallyPoke($rallyData, $activeCharacter); diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php index 7bb83f73..a9e52925 100644 --- a/app/main/controller/setup.php +++ b/app/main/controller/setup.php @@ -806,6 +806,14 @@ class Setup extends Controller { $label = ' Rally point poke Slack'; $tooltip = 'If "enabled", map admins can set a Slack channel for rally point pokes.'; break; + case 'send_history_discord_enabled': + $label = ' History log Discord'; + $tooltip = 'If "enabled", map admins can set a Discord channel were map logs get piped to.'; + break; + case 'send_rally_discord_enabled': + $label = ' Rally point poke Discord'; + $tooltip = 'If "enabled", map admins can set a Discord channel for rally point pokes.'; + break; case 'send_rally_mail_enabled': $label = ' Rally point poke Email'; $tooltip = 'If "enabled", rally point pokes can be send by Email (SMTP config + recipient address required).'; @@ -1159,8 +1167,8 @@ class Setup extends Controller { $dbVersion = 'unknown'; foreach($dbVersionParts as $dbVersionPart){ // check if this is a valid version number - // hint: MariaDB´s version is always the last valid version number... - if( version_compare( $dbVersionPart, '0.0.1', '>=' ) > 0 ){ + // hint: MariaDB´s version is NOT always the last valid version number + if( version_compare( $dbVersionPart, '1', '>' ) > 0 ){ $dbVersion = $dbVersionPart; } } diff --git a/app/main/lib/Monolog.php b/app/main/lib/Monolog.php index 667c465e..9fd71659 100644 --- a/app/main/lib/Monolog.php +++ b/app/main/lib/Monolog.php @@ -36,6 +36,8 @@ class Monolog extends \Prefab { 'mail' => 'Monolog\Handler\SwiftMailerHandler', 'slackMap' => 'lib\logging\handler\SlackMapWebhookHandler', 'slackRally' => 'lib\logging\handler\SlackRallyWebhookHandler', + 'discordMap' => 'lib\logging\handler\SlackMapWebhookHandler', // use Slack handler for Discord + 'discordRally' => 'lib\logging\handler\SlackRallyWebhookHandler', // use Slack handler for Discord 'zmq' => 'lib\logging\handler\ZMQHandler' ]; diff --git a/app/main/lib/logging/AbstractLog.php b/app/main/lib/logging/AbstractLog.php index 670daa01..2a84fc8d 100644 --- a/app/main/lib/logging/AbstractLog.php +++ b/app/main/lib/logging/AbstractLog.php @@ -250,6 +250,8 @@ abstract class AbstractLog implements LogInterface { break; case 'slackMap': case 'slackRally': + case 'discordMap': + case 'discordRally': $params = $this->getHandlerParamsSlack($handlerKey); break; default: diff --git a/app/main/model/basicmodel.php b/app/main/model/basicmodel.php index c3aa9a58..2382844b 100644 --- a/app/main/model/basicmodel.php +++ b/app/main/model/basicmodel.php @@ -19,7 +19,7 @@ abstract class BasicModel extends \DB\Cortex { /** * Hive key with DB object - * @var string + * @var string|DB\SQL */ protected $db = 'DB_PF'; diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php index dadbe64d..f256aa36 100644 --- a/app/main/model/charactermodel.php +++ b/app/main/model/charactermodel.php @@ -1081,6 +1081,11 @@ class CharacterModel extends BasicModel { return array_merge($characterDataBase, $addData); } + /** + * get all characters + * @param array $characterIds + * @return \DB\CortexCollection + */ public static function getAll($characterIds = []){ $query = [ 'active = :active AND id IN :characterIds', diff --git a/app/main/model/corporationmodel.php b/app/main/model/corporationmodel.php index 5e982f46..1650c4e9 100644 --- a/app/main/model/corporationmodel.php +++ b/app/main/model/corporationmodel.php @@ -127,24 +127,29 @@ class CorporationModel extends BasicModel { /** * get all maps for this corporation - * @return MapModel[] + * @param array $mapIds + * @param array $options + * @return array * @throws \Exception\PathfinderException */ - public function getMaps(){ + public function getMaps($mapIds = [], $options = []){ $maps = []; + $filter = ['active = ?', 1]; - $this->filter('mapCorporations', - ['active = ?', 1], - ['order' => 'created'] - ); + if( !empty($mapIds) ){ + $filter[0] .= ' AND mapId IN (?)'; + $filter[] = $mapIds; + } + + $this->filter('mapCorporations', $filter, ['order' => 'created']); if($this->mapCorporations){ $mapCount = 0; foreach($this->mapCorporations as $mapCorporation){ - if( - $mapCorporation->mapId->isActive() && - $mapCount < Config::getMapsDefaultConfig('corporation')['max_count'] - ){ + $validActive = !$options['addInactive'] ? $mapCorporation->mapId->isActive() : true; + $validMapCount = !$options['ignoreMapCount'] ? $mapCount < Config::getMapsDefaultConfig('corporation')['max_count'] : true; + + if($validActive && $validMapCount){ $maps[] = $mapCorporation->mapId; $mapCount++; } diff --git a/app/main/model/mapmodel.php b/app/main/model/mapmodel.php index 390d2815..f5300c0d 100644 --- a/app/main/model/mapmodel.php +++ b/app/main/model/mapmodel.php @@ -25,6 +25,7 @@ class MapModel extends AbstractMapTrackingModel { const DATA_CACHE_KEY_CHARACTER = 'CHARACTERS'; const ERROR_SLACK_CHANNEL = 'Invalid #Slack channel column [%s]'; + const ERROR_DISCORD_CHANNEL = 'Invalid #Discord channel column [%s]'; protected $fieldConf = [ 'active' => [ @@ -133,6 +134,24 @@ class MapModel extends AbstractMapTrackingModel { 'default' => '', 'activity-log' => true ], + 'discordUsername' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'discordWebHookURLRally' => [ + 'type' => Schema::DT_VARCHAR256, + 'nullable' => false, + 'default' => '', + 'validate' => true + ], + 'discordWebHookURLHistory' => [ + 'type' => Schema::DT_VARCHAR256, + 'nullable' => false, + 'default' => '', + 'validate' => true + ], 'systems' => [ 'has-many' => ['Model\SystemModel', 'mapId'] ], @@ -191,59 +210,65 @@ class MapModel extends AbstractMapTrackingModel { if(is_null($mapDataAll)){ // no cached map data found - $mapData = (object) []; - $mapData->id = $this->id; - $mapData->name = $this->name; - $mapData->icon = $this->icon; - $mapData->deleteExpiredConnections = $this->deleteExpiredConnections; - $mapData->deleteEolConnections = $this->deleteEolConnections; - $mapData->persistentAliases = $this->persistentAliases; + $mapData = (object) []; + $mapData->id = $this->id; + $mapData->name = $this->name; + $mapData->icon = $this->icon; + $mapData->deleteExpiredConnections = $this->deleteExpiredConnections; + $mapData->deleteEolConnections = $this->deleteEolConnections; + $mapData->persistentAliases = $this->persistentAliases; // map scope - $mapData->scope = (object) []; - $mapData->scope->id = $this->scopeId->id; - $mapData->scope->name = $this->scopeId->name; - $mapData->scope->label = $this->scopeId->label; + $mapData->scope = (object) []; + $mapData->scope->id = $this->scopeId->id; + $mapData->scope->name = $this->scopeId->name; + $mapData->scope->label = $this->scopeId->label; // map type - $mapData->type = (object) []; - $mapData->type->id = $this->typeId->id; - $mapData->type->name = $this->typeId->name; - $mapData->type->classTab = $this->typeId->classTab; + $mapData->type = (object) []; + $mapData->type->id = $this->typeId->id; + $mapData->type->name = $this->typeId->name; + $mapData->type->classTab = $this->typeId->classTab; // map logging - $mapData->logging = (object) []; - $mapData->logging->activity = $this->isActivityLogEnabled(); - $mapData->logging->history = $this->isHistoryLogEnabled(); + $mapData->logging = (object) []; + $mapData->logging->activity = $this->isActivityLogEnabled(); + $mapData->logging->history = $this->isHistoryLogEnabled(); // map Slack logging - $mapData->logging->slackHistory = $this->isSlackChannelEnabled('slackChannelHistory'); - $mapData->logging->slackRally = $this->isSlackChannelEnabled('slackChannelRally'); - $mapData->logging->slackWebHookURL = $this->slackWebHookURL; - $mapData->logging->slackUsername = $this->slackUsername; - $mapData->logging->slackIcon = $this->slackIcon; - $mapData->logging->slackChannelHistory = $this->slackChannelHistory; - $mapData->logging->slackChannelRally = $this->slackChannelRally; + $mapData->logging->slackHistory = $this->isSlackChannelEnabled('slackChannelHistory'); + $mapData->logging->slackRally = $this->isSlackChannelEnabled('slackChannelRally'); + $mapData->logging->slackWebHookURL = $this->slackWebHookURL; + $mapData->logging->slackUsername = $this->slackUsername; + $mapData->logging->slackIcon = $this->slackIcon; + $mapData->logging->slackChannelHistory = $this->slackChannelHistory; + $mapData->logging->slackChannelRally = $this->slackChannelRally; + + // map Discord logging + $mapData->logging->discordRally = $this->isDiscordChannelEnabled('discordWebHookURLRally'); + $mapData->logging->discordUsername = $this->discordUsername; + $mapData->logging->discordWebHookURLRally = $this->discordWebHookURLRally; + $mapData->logging->discordWebHookURLHistory = $this->discordWebHookURLHistory; // map mail logging - $mapData->logging->mailRally = $this->isMailSendEnabled('RALLY_SET'); + $mapData->logging->mailRally = $this->isMailSendEnabled('RALLY_SET'); // map access - $mapData->access = (object) []; - $mapData->access->character = []; - $mapData->access->corporation = []; - $mapData->access->alliance = []; + $mapData->access = (object) []; + $mapData->access->character = []; + $mapData->access->corporation = []; + $mapData->access->alliance = []; - $mapData->created = (object) []; - $mapData->created->created = strtotime($this->created); + $mapData->created = (object) []; + $mapData->created->created = strtotime($this->created); if(is_object($this->createdCharacterId)){ - $mapData->created->character = $this->createdCharacterId->getData(); + $mapData->created->character = $this->createdCharacterId->getData(); } - $mapData->updated = (object) []; - $mapData->updated->updated = strtotime($this->updated); + $mapData->updated = (object) []; + $mapData->updated->updated = strtotime($this->updated); if(is_object($this->updatedCharacterId)){ - $mapData->updated->character = $this->updatedCharacterId->getData(); + $mapData->updated->character = $this->updatedCharacterId->getData(); } // get access object data --------------------------------------------------------------------------------- @@ -315,11 +340,50 @@ class MapModel extends AbstractMapTrackingModel { * @throws \Exception\ValidationException */ protected function validate_slackWebHookURL(string $key, string $val): bool { + return $this->validate_WebHookURL($key, $val, 'slack'); + } + + /** + * validate Discord History WebHook URL + * @param string $key + * @param string $val + * @return bool + * @throws \Exception\ValidationException + */ + protected function validate_discordWebHookURLHistory(string $key, string $val): bool { + return $this->validate_WebHookURL($key, $val, 'discord'); + } + + /** + * validate Discord Rally WebHook URL + * @param string $key + * @param string $val + * @return bool + * @throws \Exception\ValidationException + */ + protected function validate_discordWebHookURLRally(string $key, string $val): bool { + return $this->validate_WebHookURL($key, $val, 'discord'); + } + + /** + * validate Slack/Discord WebHook URL + * @param string $key + * @param string $val + * @param string $type + * @return bool + * @throws \Exception\ValidationException + */ + protected function validate_WebHookURL(string $key, string $val, string $type): bool { $valid = true; if( !empty($val) ){ + $hosts = [ + 'slack' => 'hooks.slack.com', + 'discord' => 'discordapp.com' + ]; + if( !\Audit::instance()->url($val) || - parse_url($val, PHP_URL_HOST) !== 'hooks.slack.com' + parse_url($val, PHP_URL_HOST) !== $hosts[$type] ){ $valid = false; $this->throwValidationException($key); @@ -386,7 +450,6 @@ class MapModel extends AbstractMapTrackingModel { */ public function afterEraseEvent($self, $pkeys){ $self->clearCacheData(); - $self->logActivity('mapDelete'); $self->deleteLogFile(); } @@ -873,6 +936,13 @@ class MapModel extends AbstractMapTrackingModel { $log->addHandlerGroup('slackMap'); } + // send map history to Discord channel ------------------------------------------------------------------------ + $discordChannelKey = 'discordWebHookURLHistory'; + if($this->isDiscordChannelEnabled($discordChannelKey)){ + $log->addHandler('discordMap', null, $this->getDiscordWebHookConfig($discordChannelKey)); + $log->addHandlerGroup('discordMap'); + } + // update map activity ---------------------------------------------------------------------------------------- $log->logActivity($this->isActivityLogEnabled()); @@ -959,6 +1029,34 @@ class MapModel extends AbstractMapTrackingModel { return $enabled; } + /** + * check if "Discord WebHook" is enabled for this map type + * @param string $channel + * @return bool + * @throws PathfinderException + */ + public function isDiscordChannelEnabled(string $channel): bool { + $enabled = false; + // check global Slack status + if((bool)Config::getPathfinderData('discord.status')){ + // check global map default config for this channel + switch($channel){ + case 'discordWebHookURLHistory': $defaultMapConfigKey = 'send_history_discord_enabled'; break; + case 'discordWebHookURLRally': $defaultMapConfigKey = 'send_rally_discord_enabled'; break; + default: throw new PathfinderException(sprintf(self::ERROR_DISCORD_CHANNEL, $channel)); + } + + if((bool) Config::getMapsDefaultConfig($this->typeId->name)[$defaultMapConfigKey]){ + $config = $this->getDiscordWebHookConfig($channel); + if($config->slackWebHookURL){ + $enabled = true; + } + } + } + + return $enabled; + } + /** * check if "E-Mail" Log is enabled for this map * @param string $type @@ -1018,6 +1116,20 @@ class MapModel extends AbstractMapTrackingModel { return $config; } + /** + * get Config for Discord WebHook cURL calls + * @param string $channel + * @return \stdClass + */ + public function getDiscordWebHookConfig(string $channel = ''): \stdClass { + $config = (object) []; + $config->slackUsername = $this->discordUsername; + if($channel && $this->exists($channel)){ + $config->slackWebHookURL = $this->$channel . '/slack'; + } + return $config; + } + /** * get Config for SMTP connection and recipient address * @param string $type @@ -1222,9 +1334,12 @@ class MapModel extends AbstractMapTrackingModel { /** * @param CharacterModel|null $characterModel - * @return false|ConnectionModel + * @return false|MapModel */ public function save(CharacterModel $characterModel = null){ + /** + * @var $mapModel MapModel + */ $mapModel = parent::save($characterModel); // check if map type has changed and clear access objects @@ -1241,4 +1356,18 @@ class MapModel extends AbstractMapTrackingModel { return $mapModel; } + /** + * get all maps + * @param array $mapIds + * @return \DB\CortexCollection + */ + public static function getAll($mapIds = []){ + $query = [ + 'active = :active AND id IN :mapIds', + ':active' => 1, + ':mapIds' => $mapIds + ]; + + return (new self())->find($query); + } } diff --git a/app/main/model/systemmodel.php b/app/main/model/systemmodel.php index 884b4b95..879d81ca 100644 --- a/app/main/model/systemmodel.php +++ b/app/main/model/systemmodel.php @@ -565,6 +565,7 @@ class SystemModel extends AbstractMapTrackingModel { /** * send rally point poke to various "APIs" * -> send to a Slack channel + * -> send to a Discord channel * -> send to an Email * @param array $rallyData * @param CharacterModel $characterModel @@ -585,6 +586,17 @@ class SystemModel extends AbstractMapTrackingModel { $log->addHandler('slackRally', null, $this->getMap()->getSlackWebHookConfig($slackChannelKey)); } + // Discord poke --------------------------------------------------------------------------- + $discordChannelKey = 'discordWebHookURLRally'; + if( + $rallyData['pokeDiscord'] === true && + $this->getMap()->isDiscordChannelEnabled($discordChannelKey) + ){ + $isValidLog = true; + + $log->addHandler('discordRally', null, $this->getMap()->getDiscordWebHookConfig($discordChannelKey)); + } + // Mail poke ------------------------------------------------------------------------------ $mailAddressKey = 'RALLY_SET'; if( diff --git a/app/pathfinder.ini b/app/pathfinder.ini index 4d1f156a..c5f1ffcc 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -3,7 +3,7 @@ [PATHFINDER] NAME = Pathfinder ; installed version (used for CSS/JS cache busting) -VERSION = v1.3.1 +VERSION = v1.3.2 ; contact information [optional] CONTACT = https://github.com/exodus4d ; public contact email [optional] @@ -36,6 +36,11 @@ ALLIANCE = ; Global Slack API status, check PATHFINDER.MAP section for individual control (0=disabled, 1=enabled) STATUS = 1 +; Slack API integration =========================================================================== +[PATHFINDER.DISCORD] +; Global Discord API status, check PATHFINDER.MAP section for individual control (0=disabled, 1=enabled) +STATUS = 1 + ; View ============================================================================================ [PATHFINDER.VIEW] ; static page templates @@ -82,28 +87,34 @@ LOG_ACTIVITY_ENABLED = 1 LOG_HISTORY_ENABLED = 1 SEND_HISTORY_SLACK_ENABLED = 0 SEND_RALLY_SLACK_ENABLED = 1 +SEND_HISTORY_DISCORD_ENABLED = 0 +SEND_RALLY_DISCORD_ENABLED = 1 SEND_RALLY_Mail_ENABLED = 0 [PATHFINDER.MAP.CORPORATION] LIFETIME = 99999 -MAX_COUNT = 3 -MAX_SHARED = 3 +MAX_COUNT = 5 +MAX_SHARED = 4 MAX_SYSTEMS = 100 LOG_ACTIVITY_ENABLED = 1 LOG_HISTORY_ENABLED = 1 SEND_HISTORY_SLACK_ENABLED = 1 SEND_RALLY_SLACK_ENABLED = 1 +SEND_HISTORY_DISCORD_ENABLED = 1 +SEND_RALLY_DISCORD_ENABLED = 1 SEND_RALLY_Mail_ENABLED = 0 [PATHFINDER.MAP.ALLIANCE] LIFETIME = 99999 -MAX_COUNT = 3 +MAX_COUNT = 4 MAX_SHARED = 2 MAX_SYSTEMS = 100 LOG_ACTIVITY_ENABLED = 0 LOG_HISTORY_ENABLED = 1 SEND_HISTORY_SLACK_ENABLED = 1 SEND_RALLY_SLACK_ENABLED = 1 +SEND_HISTORY_DISCORD_ENABLED = 1 +SEND_RALLY_DISCORD_ENABLED = 1 SEND_RALLY_Mail_ENABLED = 0 ; Route search ==================================================================================== diff --git a/composer.json b/composer.json index a859c312..2cd2faa7 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,6 @@ "monolog/monolog": "1.*", "websoftwares/monolog-zmq-handler": "0.2.*", "swiftmailer/swiftmailer": "^6.0", - "exodus4d/pathfinder_esi": "dev-master#v1.2.1" + "exodus4d/pathfinder_esi": "dev-master#v1.2.2" } } diff --git a/js/app/login.js b/js/app/login.js index 82d87079..874afd89 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -16,7 +16,7 @@ define([ 'dialog/account_settings', 'dialog/notification', 'dialog/manual', - 'dialog/releases', + 'dialog/changelog', 'dialog/credit' ], function($, Init, Util, Render, Gallery, bootbox) { @@ -81,7 +81,7 @@ define([ */ let setVersionLinkObserver = function(){ $('.' + config.navigationVersionLinkClass).off('click').on('click', function(e){ - $.fn.releasesDialog(); + $.fn.changelogsDialog(); }); }; diff --git a/js/app/map/layout.js b/js/app/map/layout.js new file mode 100644 index 00000000..d0b7a4c9 --- /dev/null +++ b/js/app/map/layout.js @@ -0,0 +1,400 @@ +define(() => { + 'use strict'; + + class Position { + + constructor(config) { + this._defaultConfig = { + container: null, // parent DOM container element + center: null, // DOM elements that works as center + elementClass: 'pf-system', // class for all elements + defaultGapX: 50, + defaultGapY: 50, + gapX: 50, // leave gap between elements (x-axis) + gapY: 50, // leave gap between elements (y-axis) + loops: 2, // max loops around "center" for search + grid: false, // set to [20, 20] to force grid snapping + newElementWidth: 100, // width for new element + newElementHeight: 22, // height for new element + debug: false, // render debug elements + debugElementClass: 'pf-system-debug' // class for debug elements + }; + + this._config = Object.assign({}, this._defaultConfig, config); + + /** + * convert degree into radial unit + * @param deg + * @returns {number} + * @private + */ + this._degToRad = (deg) => { + return deg * Math.PI / 180; + }; + + /** + * get element dimension/position of a DOM element + * @param element + * @returns {*} + * @private + */ + this._getElementDimension = (element) => { + let dim = null; + + let left = 0; + let top = 0; + let a = 0; + let b = 0; + let width = this._config.newElementWidth; + let height = this._config.newElementHeight; + + if(Array.isArray(element)){ + // xy coordinates + let point = [ + element[0] ? parseInt(element[0], 10) : 0, + element[1] ? parseInt(element[1], 10) : 0 + ]; + + if(this._config.grid){ + point = this._transformPointToGrid(point); + } + + left = point[0]; + top = point[1]; + a = this._config.gapX ; + b = this._config.gapY ; + }else if(element){ + // DOM element + left = element.style.left ? parseInt(element.style.left, 10) : 0; + top = element.style.top ? parseInt(element.style.top, 10) : 0; + a = parseInt((element.offsetWidth / 2).toString(), 10) + this._config.gapX; + b = parseInt((element.offsetHeight / 2).toString(), 10) + this._config.gapY; + width = element.offsetWidth; + height = element.offsetHeight; + } + + // add "gap" to a and b in order to have some space around elements + return { + left: left, + top: top, + a: a, + b: b, + width: width, + height: height + }; + }; + + /** + * get x/y coordinate on an eclipse around a 2D area by a given radial angle + * @param dim + * @param angle + * @returns {*} + * @private + */ + this._getEllipseCoordinates = (dim, angle) => { + let coordinates = null; + if(dim){ + angle = this._degToRad(angle); + coordinates = { + x: Math.round((dim.a * dim.b) / Math.sqrt(Math.pow(dim.b, 2) + Math.pow(dim.a, 2) * Math.pow(Math.tan(angle), 2) )), + y: Math.round((dim.a * dim.b) / Math.sqrt(Math.pow(dim.a, 2) + Math.pow(dim.b, 2) / Math.pow(Math.tan(angle), 2) )) + }; + + // invert coordinate based on quadrant ------------------------------------------------------------ + if( angle > (Math.PI / 2) && angle < (3 * Math.PI / 2) ){ + coordinates.x = coordinates.x * -1; + } + + if( angle > Math.PI && angle < (2 * Math.PI) ){ + coordinates.y = coordinates.y * -1; + } + } + return coordinates; + }; + + /** + * get dimensions of all surrounding elements + * @returns {Array} + * @private + */ + this._getAllElementDimensions = () => { + let dimensions = []; + let surroundingElements = this._getContainer().getElementsByClassName(this._config.elementClass); + for(let element of surroundingElements){ + dimensions.push(this._getElementDimension(element)); + } + return dimensions; + }; + + /** + * transform a x/y point into a x/y point that snaps to grid + * @param point + * @returns {*} + * @private + */ + this._transformPointToGrid = (point) => { + point[0] = Math.floor(point[0] / this._config.grid[0]) * this._config.grid[0]; + point[1] = Math.floor(point[1] / this._config.grid[1]) * this._config.grid[1]; + return point; + }; + + /** + * Transform a x/y coordinate into a 2D element with width/height + * @param centerDimension + * @param coordinate + * @returns {*} + * @private + */ + this._transformCoordinate = (centerDimension, coordinate) => { + let dim = null; + if(centerDimension && coordinate){ + let left = 0; + let top = 0; + + // calculate left/top based on coordinate system quadrant ----------------------------------------- + // -> flip horizontal in Q2 and Q3 + if(coordinate.x >= 0 && coordinate.y > 0){ + // 1. quadrant + left = centerDimension.left + centerDimension.a - this._config.gapX + coordinate.x; + top = centerDimension.top + 2 * (centerDimension.b - this._config.gapY) - Math.abs(coordinate.y) - this._config.newElementHeight; + }else if(coordinate.x < 0 && coordinate.y > 0){ + // 2. quadrant + left = centerDimension.left + centerDimension.a - this._config.gapX + coordinate.x - this._config.newElementWidth; + top = centerDimension.top + 2 * (centerDimension.b - this._config.gapY) - Math.abs(coordinate.y) - this._config.newElementHeight; + }else if(coordinate.x < 0 && coordinate.y <= 0){ + // 3. quadrant + left = centerDimension.left + centerDimension.a - this._config.gapX + coordinate.x - this._config.newElementWidth; + top = centerDimension.top + Math.abs(coordinate.y); + }else{ + // 4. quadrant + left = centerDimension.left + centerDimension.a - this._config.gapX + coordinate.x; + top = centerDimension.top + Math.abs(coordinate.y); + } + + // center horizontal for x = 0 coordinate (top and bottom element) -------------------------------- + if(coordinate.x === 0){ + left -= Math.round(this._config.newElementWidth / 2); + } + + // transform to grid coordinates (if grid snapping is enabled) ------------------------------------ + if(this._config.grid){ + let point = this._transformPointToGrid([left, top]); + left = point[0]; + top = point[1]; + } + + dim = { + left: left, + top: top, + width: this._config.newElementWidth, + height: this._config.newElementHeight + }; + } + + return dim; + }; + + /** + * calc overlapping percent of two given dimensions + * @param dim1 + * @param dim2 + * @returns {number} + * @private + */ + this._percentCovered = (dim1, dim2) => { + let percent = 0; + + if ( + (dim1.left <= dim2.left) && + (dim1.top <= dim2.top) && + ((dim1.left + dim1.width) >= (dim2.left + dim2.width)) && + ((dim1.top + dim1.height) > (dim2.top + dim2.height)) + ) { + // The whole thing is covering the whole other thing + percent = 100; + }else{ + // Only parts may be covered, calculate percentage + dim1.right = dim1.left + dim1.width; + dim1.bottom = dim1.top + dim1.height; + dim2.right = dim2.left + dim2.width; + dim2.bottom = dim2.top + dim2.height; + + let l = Math.max(dim1.left, dim2.left); + let r = Math.min(dim1.right, dim2.right); + let t = Math.max(dim1.top, dim2.top); + let b = Math.min(dim1.bottom, dim2.bottom); + + if (b >= t && r >= l) { + percent = (((r - l) * (b - t)) / (dim2.width * dim2.height)) * 100; + } + } + return percent; + }; + + /** + * checks whether dim1 is partially overlapped by any other element + * @param dim1 + * @param dimensionContainer + * @param allDimensions + * @returns {boolean} + * @private + */ + this._isOverlapping = (dim1, dimensionContainer, allDimensions) => { + let isOverlapping = false; + if(dim1){ + if(this._percentCovered(dimensionContainer, dim1 ) === 100){ + // element is within parent container + for(let dim2 of allDimensions){ + let percentCovered = this._percentCovered(dim1, dim2); + if(percentCovered){ + isOverlapping = true; + // render debug element + this._showDebugElement(dim1, percentCovered); + break; + } + } + }else{ + isOverlapping = true; + this._showDebugElement(dim1, 100); + } + }else{ + isOverlapping = true; + } + + return isOverlapping; + }; + + /** + * find all dimensions around a centerDimension that are not overlapped by other elements + * @param maxResults + * @param steps + * @param allDimensions + * @param loops + * @returns {Array} + * @private + */ + this._findDimensions = (maxResults, steps, allDimensions, loops) => { + let dimensions = []; + let start = 0; + let end = 360; + let angle = end / steps; + let dimensionContainer = this._getElementDimension(this._getContainer()); + steps = steps || 1; + loops = loops || 1; + + if(loops === 1){ + // check center element + let centerDimension = this._getElementDimension(this._config.center); + if (!this._isOverlapping(centerDimension, dimensionContainer, allDimensions)) { + dimensions.push({ + left: centerDimension.left, + top: centerDimension.top, + width: centerDimension.width, + height: centerDimension.height + }); + // render debug element + this._showDebugElement(centerDimension, 0); + + maxResults--; + } + } + + // increase the "gab" between center element and potential found dimensions... + // ... for each recursive loop call, to get an elliptical cycle beyond + this._config.gapX = this._config.defaultGapX + (loops - 1) * 20; + this._config.gapY = this._config.defaultGapY + (loops - 1) * 20; + let centerDimension = this._getElementDimension(this._config.center); + + while(maxResults > 0 && start < end){ + // get all potential coordinates on an eclipse around a given "centerElementDimension" + let coordinate = this._getEllipseCoordinates(centerDimension, end); + // transform relative x/y coordinate into a absolute 2D area + let checkDimension = this._transformCoordinate(centerDimension, coordinate); + if (!this._isOverlapping(checkDimension, dimensionContainer, allDimensions)) { + dimensions.push({ + left: checkDimension.left, + top: checkDimension.top, + width: checkDimension.width, + height: checkDimension.height + }); + // render debug element + this._showDebugElement(checkDimension, 0); + + maxResults--; + } + end -= angle; + } + + if(maxResults > 0 && loops < this._config.loops){ + loops++; + steps *= 2; + dimensions = dimensions.concat(this._findDimensions(maxResults, steps, allDimensions, loops)); + } + + return dimensions; + }; + + /** + * get container (parent) element + * @returns {*} + * @private + */ + this._getContainer = () => { + return this._config.container ? this._config.container : document.body; + }; + + /** + * render debug element into parent container + * -> checks overlapping dimension with other elements + * @param dimension + * @param percentCovered + * @private + */ + this._showDebugElement = (dimension, percentCovered) => { + if(this._config.debug){ + let element = document.createElement('div'); + element.style.left = dimension.left + 'px'; + element.style.top = dimension.top + 'px'; + element.style.width = dimension.width + 'px'; + element.style.height = dimension.height + 'px'; + element.style.backgroundColor = Boolean(percentCovered) ? 'rgba(255,0,0,0.1)' : 'rgba(0,255,0,0.1)'; + element.innerHTML = Math.round(percentCovered * 100) / 100 + '%'; + element.classList.add(this._config.debugElementClass); + this._getContainer().appendChild(element); + } + }; + + /** + * hide all debug elements + * @private + */ + this._hideDebugElements = () => { + let debugElements = this._getContainer().getElementsByClassName(this._config.debugElementClass); + while(debugElements.length > 0){ + debugElements[0].parentNode.removeChild(debugElements[0]); + } + }; + + + // public functions --------------------------------------------------------------------------------------- + + /** + * search for surrounding, non overlapping dimensions + * @param maxResults + * @param steps + * @returns {Array} + */ + this.findNonOverlappingDimensions = (maxResults, steps) => { + this._hideDebugElements(); + // element dimensions that exist and should be checked for overlapping + let allDimensions = this._getAllElementDimensions(); + + return this._findDimensions(maxResults, steps, allDimensions); + }; + } + } + + return { + Position: Position + }; +}); \ No newline at end of file diff --git a/js/app/map/map.js b/js/app/map/map.js index 55ee0b9a..b2117d81 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -10,6 +10,7 @@ define([ 'bootbox', 'app/map/util', 'app/map/system', + 'app/map/layout', 'app/map/magnetizing', 'app/map/scrollbar', 'dragToSelect', @@ -17,7 +18,7 @@ define([ 'app/map/contextmenu', 'app/map/overlay', 'app/map/local' -], function($, Init, Util, Render, bootbox, MapUtil, System, MagnetizerWrapper) { +], function($, Init, Util, Render, bootbox, MapUtil, System, Layout, MagnetizerWrapper) { 'use strict'; @@ -25,7 +26,6 @@ define([ zIndexCounter: 110, maxActiveConnections: 8, - mapSnapToGrid: false, // "Snap to Grid" feature for drag&drop systems on map (optional) mapWrapperClass: 'pf-map-wrapper', // wrapper div (scrollable) mapClass: 'pf-map', // class for all maps @@ -1569,6 +1569,7 @@ define([ // get new position newPosition = System.calculateNewSystemPosition(sourceSystem); + }else{ // check mouse cursor position (add system to map) newPosition = { @@ -2062,7 +2063,7 @@ define([ let mapContainer = $( map.getContainer() ); let systemHeadExpand = $( system.find('.' + config.systemHeadExpandClass) ); let systemBody = $( system.find('.' + config.systemBodyClass) ); - + let grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension]; // map overlay will be set on "drag" start let mapOverlayTimer = null; @@ -2083,7 +2084,7 @@ define([ // check if grid-snap is enable -> this enables napping for !CURRENT! Element if( mapContainer.hasClass(MapUtil.config.mapGridClass) ){ - params.drag.params.grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension]; + params.drag.params.grid = grid; }else{ delete( params.drag.params.grid ); } @@ -2661,6 +2662,24 @@ define([ switch(action){ case 'add_system': // add new system dialog + let grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension]; + let positionFinder = new Layout.Position({ + container: currentMapElement[0], + center: [position.x, position.y], + loops: 5, + defaultGapX: 10, + defaultGapY: 10, + grid: currentMapElement.hasClass(MapUtil.config.mapGridClass) ? grid : false, + debug: false + }); + + let dimensions = positionFinder.findNonOverlappingDimensions(1, 8); + + if(dimensions.length){ + position.x = dimensions[0].left; + position.y = dimensions[0].top; + } + showNewSystemDialog(currentMap, {position: position}); break; case 'select_all': diff --git a/js/app/map/system.js b/js/app/map/system.js index 3aa6b03a..52da853b 100644 --- a/js/app/map/system.js +++ b/js/app/map/system.js @@ -8,8 +8,9 @@ define([ 'app/init', 'app/util', 'bootbox', - 'app/map/util' -], ($, Init, Util, bootbox, MapUtil) => { + 'app/map/util', + 'app/map/layout' +], ($, Init, Util, bootbox, MapUtil, Layout) => { 'use strict'; let config = { @@ -24,6 +25,7 @@ define([ dialogRallyPokeDesktopId: 'pf-rally-dialog-poke-desktop', // id for "desktop" poke checkbox dialogRallyPokeSlackId: 'pf-rally-dialog-poke-slack', // id for "Slack" poke checkbox + dialogRallyPokeDiscordId: 'pf-rally-dialog-poke-discord', // id for "Discord" poke checkbox dialogRallyPokeMailId: 'pf-rally-dialog-poke-mail', // id for "mail" poke checkbox dialogRallyMessageId: 'pf-rally-dialog-message', // id for "message" textarea @@ -88,11 +90,13 @@ define([ dialogRallyPokeDesktopId: config.dialogRallyPokeDesktopId, dialogRallyPokeSlackId: config.dialogRallyPokeSlackId, + dialogRallyPokeDiscordId: config.dialogRallyPokeDiscordId, dialogRallyPokeMailId: config.dialogRallyPokeMailId, dialogRallyMessageId: config.dialogRallyMessageId , desktopRallyEnabled: true, slackRallyEnabled: Boolean(Util.getObjVal(mapData, 'config.logging.slackRally')), + discordRallyEnabled: Boolean(Util.getObjVal(mapData, 'config.logging.discordRally')), mailRallyEnabled: Boolean(Util.getObjVal(mapData, 'config.logging.mailRally')), dialogRallyMessageDefault: config.dialogRallyMessageDefault, @@ -132,6 +136,8 @@ define([ } }); + rallyDialog.initTooltips(); + // after modal is shown ================================================================================== rallyDialog.on('shown.bs.modal', function(e){ // set event for checkboxes @@ -293,21 +299,45 @@ define([ /** * calculate the x/y coordinates for a new system - relativ to a source system * @param sourceSystem + * @param grid * @returns {{x: *, y: *}} */ let calculateNewSystemPosition = function(sourceSystem){ + let mapContainer = sourceSystem.parent(); + let grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension]; - // related system is available - let currentX = sourceSystem.css('left'); - let currentY = sourceSystem.css('top'); + let x = 0; + let y = 0; - // remove "px" - currentX = parseInt( currentX.substring(0, currentX.length - 2) ); - currentY = parseInt( currentY.substring(0, currentY.length - 2) ); + let positionFinder = new Layout.Position({ + container: mapContainer[0], + center: sourceSystem[0], + loops: 4, + grid: mapContainer.hasClass(MapUtil.config.mapGridClass) ? grid : false, + debug: false + }); + + let dimensions = positionFinder.findNonOverlappingDimensions(1, 16); + if(dimensions.length){ + //... empty map space found + x = dimensions[0].left; + y = dimensions[0].top; + }else{ + //... fallback + // related system is available + let currentX = sourceSystem.css('left'); + let currentY = sourceSystem.css('top'); + + // remove "px" + currentX = parseInt( currentX.substring(0, currentX.length - 2) ); + currentY = parseInt( currentY.substring(0, currentY.length - 2) ); + x = currentX + config.newSystemOffset.x; + y = currentY + config.newSystemOffset.y; + } let newPosition = { - x: currentX + config.newSystemOffset.x, - y: currentY + config.newSystemOffset.y + x: x, + y: y }; return newPosition; diff --git a/js/app/map/util.js b/js/app/map/util.js index 0b927aa3..f67793a3 100644 --- a/js/app/map/util.js +++ b/js/app/map/util.js @@ -62,48 +62,39 @@ define([ * @returns {Array} */ let getMapTypes = (filterByUser) => { - let mapTypes = []; - - $.each(Init.mapTypes, function(prop, data){ - // skip "default" type -> just for 'add' icon - if(data.label.length > 0){ - let tempData = data; - tempData.name = prop; - mapTypes.push(tempData); - } - }); + let mapTypes = Object.assign({}, Init.mapTypes); if(filterByUser === true){ - let corporationId = Util.getCurrentUserInfo('corporationId'); - let allianceId = Util.getCurrentUserInfo('allianceId'); - let authorizedMapTypes = []; - // check if character data exists - if(corporationId > 0) { - authorizedMapTypes.push('corporation'); - } - if(allianceId > 0){ - authorizedMapTypes.push('alliance'); - } + let checkMapTypes = ['private', 'corporation', 'alliance']; - // private maps are always allowed - authorizedMapTypes.push('private'); - - // compare "all" map types with "authorized" types - let tempMapTypes = []; - for(let i = 0; i < mapTypes.length; i++){ - for(let j = 0; j < authorizedMapTypes.length; j++){ - if(mapTypes[i].name === authorizedMapTypes[j]){ - tempMapTypes.push(mapTypes[i]); - break; + for(let i = 0; i < checkMapTypes.length; i++){ + let objectId = Util.getCurrentUserInfo(checkMapTypes[i] + 'Id'); + if(objectId > 0) { + // check if User could add new map with a mapType + let currentObjectMapData = Util.filterCurrentMapData('config.type.id', Util.getObjVal(mapTypes, checkMapTypes[i] + '.id')); + let maxCountObject = Util.getObjVal(mapTypes, checkMapTypes[i] + '.defaultConfig.max_count'); + if(currentObjectMapData.length < maxCountObject){ + authorizedMapTypes.push(checkMapTypes[i]); } - } } - mapTypes = tempMapTypes; + + for(let mapType in mapTypes) { + if(authorizedMapTypes.indexOf(mapType) < 0){ + delete( mapTypes[mapType] ); + } + } } - return mapTypes; + // convert to array + let mapTypesFlat = []; + for(let mapType in mapTypes){ + mapTypes[mapType].name = mapType; + mapTypesFlat.push(mapTypes[mapType]); + } + + return mapTypesFlat; }; /** diff --git a/js/app/mappage.js b/js/app/mappage.js index a7c4bccf..8c327729 100644 --- a/js/app/mappage.js +++ b/js/app/mappage.js @@ -10,10 +10,10 @@ define([ 'app/logging', 'app/page', 'app/map/worker', + 'app/module_map', 'app/key', - 'app/ui/form_element', - 'app/module_map' -], ($, Init, Util, Render, Logging, Page, MapWorker) => { + 'app/ui/form_element' +], ($, Init, Util, Render, Logging, Page, MapWorker, ModuleMap) => { 'use strict'; @@ -69,6 +69,7 @@ define([ Init.routes = initData.routes; Init.url = initData.url; Init.slack = initData.slack; + Init.discord = initData.discord; Init.routeSearch = initData.routeSearch; Init.programMode = initData.programMode; @@ -111,12 +112,12 @@ define([ switch(MsgWorkerMessage.task()){ case 'mapUpdate': Util.updateCurrentMapData( MsgWorkerMessage.data() ); - mapModule.updateMapModule(); + ModuleMap.updateMapModule(mapModule); break; case 'mapAccess': case 'mapDeleted': Util.deleteCurrentMapData( MsgWorkerMessage.data() ); - mapModule.updateMapModule(); + ModuleMap.updateMapModule(mapModule); break; } @@ -265,22 +266,24 @@ define([ Util.setCurrentMapData(data.mapData); // load/update main map module - mapModule.updateMapModule(); + ModuleMap.updateMapModule(mapModule).then(() => { + // map update done, init new trigger - // get the current update delay (this can change if a user is inactive) - let mapUpdateDelay = Util.getCurrentTriggerDelay( logKeyServerMapData, 0 ); + // get the current update delay (this can change if a user is inactive) + let mapUpdateDelay = Util.getCurrentTriggerDelay( logKeyServerMapData, 0 ); - // init new trigger - initMapUpdatePing(false); + // init new trigger + initMapUpdatePing(false); - // initial start for the userUpdate trigger - // this should only be called at the first time! - if(updateTimeouts.userUpdate === 0){ - // start user update trigger after map loaded - updateTimeouts.userUpdate = setTimeout(() => { - triggerUserUpdatePing(); - }, 1000); - } + // initial start for the userUpdate trigger + // this should only be called at the first time! + if(updateTimeouts.userUpdate === 0){ + // start user update trigger after map loaded + updateTimeouts.userUpdate = setTimeout(() => { + triggerUserUpdatePing(); + }, 1000); + } + }); } }).fail(handleAjaxErrorResponse); @@ -288,7 +291,6 @@ define([ // skip this mapUpdate trigger and init next one initMapUpdatePing(false); } - }; // ping for user data update ======================================================= diff --git a/js/app/module_map.js b/js/app/module_map.js index 220b7fe3..e5cb77f5 100644 --- a/js/app/module_map.js +++ b/js/app/module_map.js @@ -34,8 +34,9 @@ define([ mapTabBarId: 'pf-map-tabs', // id for map tab bar mapTabIdPrefix: 'pf-map-tab-', // id prefix for a map tab mapTabClass: 'pf-map-tab', // class for a map tab - mapTabLinkTextClass: 'nav-tabs-link', // class for span elements in a tab + mapTabDragHandlerClass: 'pf-map-tab-handler', // class for drag handler mapTabIconClass: 'pf-map-tab-icon', // class for map icon + mapTabLinkTextClass: 'nav-tabs-link', // class for span elements in a tab mapTabSharedIconClass: 'pf-map-tab-shared-icon', // class for map shared icon mapTabContentClass: 'pf-map-tab-content', // class for tab content container mapTabContentSystemInfoClass: 'pf-map-tab-content-system', @@ -77,28 +78,25 @@ define([ }; /** - * set Tab Observer, events are triggered within map.js + * set mapContent Observer, events are triggered within map.js + * @param tabElement */ - $.fn.setTabContentObserver = function(){ - return this.each(function(){ - let tabContentElement = $(this); - // update Tab Content with system data information - tabContentElement.on('pf:drawSystemModules', function(e){ - drawSystemModules($(e.target)); - }); - - tabContentElement.on('pf:removeSystemModules', function(e){ - removeSystemModules($(e.target)); - }); - - tabContentElement.on('pf:drawConnectionModules', function(e, data){ - drawConnectionModules($(e.target), data); - }); - - tabContentElement.on('pf:removeConnectionModules', function(e){ - removeConnectionModules($(e.target)); - }); + let setMapContentObserver = (tabElement) => { + tabElement.on('pf:drawSystemModules', '.' + config.mapTabContentClass, function(e){ + drawSystemModules($(e.target)); }); + + tabElement.on('pf:removeSystemModules', '.' + config.mapTabContentClass, function(e){ + removeSystemModules($(e.target)); + }); + + tabElement.on('pf:drawConnectionModules', '.' + config.mapTabContentClass, function(e, data){ + drawConnectionModules($(e.target), data); + }); + + tabElement.on('pf:removeConnectionModules', '.' + config.mapTabContentClass, function(e){ + removeConnectionModules($(e.target)); + }) ; }; /** @@ -167,27 +165,6 @@ define([ * @param data */ let drawModule = (parentElement, Module, mapId, data) => { - /** - * get module position within its parentElement - * @param parentElement - * @param Module - * @param defaultPosition - * @returns {number} - */ - let getModulePosition = (parentElement, Module, defaultPosition) => { - let position = 0; - if(defaultPosition > 0){ - parentElement.children().each((i, moduleElement) => { - position = i + 1; - let tempPosition = parseInt(moduleElement.getAttribute('data-position')) || 0; - if(tempPosition >= defaultPosition){ - position--; - return false; - } - }); - } - return position; - }; /** * show/render a Module @@ -225,7 +202,7 @@ define([ } // find correct position for new moduleElement ---------------------------------------------------- - let position = getModulePosition(this.parentElement, Module, defaultPosition); + let position = getModulePosition(this.parentElement, defaultPosition); this.moduleElement.attr('data-position', defaultPosition); this.moduleElement.attr('data-module', Module.config.moduleName); @@ -238,7 +215,7 @@ define([ this.parentElement.prepend(this.moduleElement); } - if(typeof Module.beforeShow === 'function'){ + if (typeof Module.beforeShow === 'function') { Module.beforeShow(this.moduleElement, moduleElement.data('data')); } @@ -266,11 +243,11 @@ define([ // check if module already exists let moduleElement = parentElement.find('.' + Module.config.moduleTypeClass); - if(moduleElement.length > 0){ + if (moduleElement.length > 0) { removeModule(moduleElement, Module, () => { showPanel(parentElement, Module, mapId, data); }); - }else{ + } else { showPanel(parentElement, Module, mapId, data); } }; @@ -428,7 +405,6 @@ define([ // remember height if(! moduleElement.data('origHeight')){ - moduleElement.data('origHeight', moduleElement.outerHeight()); } @@ -461,13 +437,14 @@ define([ /** * load all structure elements into a TabsContent div (tab body) + * @param tabContentElements */ let initContentStructure = (tabContentElements) => { tabContentElements.each(function(){ let tabContentElement = $(this); let mapId = parseInt( tabContentElement.attr('data-mapid') ); - // "add" tab does not need a structure and obervers... + // "add" tab does not need a structure and observer... if(mapId > 0){ let contentStructure = $('
', { class: ['row', config.mapTabContentRow].join(' ') @@ -484,6 +461,7 @@ define([ // append grid structure tabContentElement.append(contentStructure); + // set content structure observer setContentStructureObserver(contentStructure, mapId); } }); @@ -492,9 +470,10 @@ define([ /** * get a fresh tab element * @param options + * @param currentUserData * @returns {*|jQuery|HTMLElement} */ - let getTabElement = (options) => { + let getMapTabElement = (options, currentUserData) => { let tabElement = $('
', { id: config.mapTabElementId }); @@ -502,7 +481,7 @@ define([ let tabBar = $('