diff --git a/application/Espo/Classes/AppParams/AddressCountryData.php b/application/Espo/Classes/AppParams/AddressCountryData.php new file mode 100644 index 0000000000..01d925c934 --- /dev/null +++ b/application/Espo/Classes/AppParams/AddressCountryData.php @@ -0,0 +1,48 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\AppParams; + +use Espo\Core\Utils\Address\CountryDataProvider; +use Espo\Tools\App\AppParam; + +class AddressCountryData implements AppParam +{ + public function __construct( + private CountryDataProvider $provider + ) {} + + /** + * @return array{list: string[], preferredList: string[]} + */ + public function get(): array + { + return $this->provider->get(); + } +} diff --git a/application/Espo/Classes/FieldSanitizers/StringUpperCase.php b/application/Espo/Classes/FieldSanitizers/StringUpperCase.php new file mode 100644 index 0000000000..8b4a05a44c --- /dev/null +++ b/application/Espo/Classes/FieldSanitizers/StringUpperCase.php @@ -0,0 +1,56 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldSanitizers; + +use Espo\Core\FieldSanitize\Sanitizer; +use Espo\Core\FieldSanitize\Sanitizer\Data; + +/** + * @noinspection PhpUnused + */ +class StringUpperCase implements Sanitizer +{ + public function sanitize(Data $data, string $field): void + { + if (!$data->has($field)) { + return; + } + + $value = $data->get($field); + + if (!is_string($value)) { + return; + } + + $value = mb_strtoupper($value); + + $data->set($field, $value); + } +} diff --git a/application/Espo/Classes/Select/AddressCountry/PreferredNameOrderer.php b/application/Espo/Classes/Select/AddressCountry/PreferredNameOrderer.php new file mode 100644 index 0000000000..0fd53d0e06 --- /dev/null +++ b/application/Espo/Classes/Select/AddressCountry/PreferredNameOrderer.php @@ -0,0 +1,45 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\Select\AddressCountry; + +use Espo\Core\Select\Order\Item; +use Espo\Core\Select\Order\Orderer; +use Espo\ORM\Query\Part\Order; +use Espo\ORM\Query\SelectBuilder; + +class PreferredNameOrderer implements Orderer +{ + public function apply(SelectBuilder $queryBuilder, Item $item): void + { + $queryBuilder + ->order('isPreferred', $item->getOrder() === Order::ASC ? Order::DESC : Order::ASC) + ->order('name', $item->getOrder()); + } +} diff --git a/application/Espo/Controllers/AddressCountry.php b/application/Espo/Controllers/AddressCountry.php new file mode 100644 index 0000000000..56cc5f0419 --- /dev/null +++ b/application/Espo/Controllers/AddressCountry.php @@ -0,0 +1,50 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Controllers; + +use Espo\Core\Controllers\RecordBase; +use Espo\Tools\Address\CountryDefaultsPopulator; + +class AddressCountry extends RecordBase +{ + protected function checkAccess(): bool + { + return $this->user->isAdmin(); + } + + public function postActionPopulateDefaults(): bool + { + $populate = $this->injectableFactory->create(CountryDefaultsPopulator::class); + + $populate->populate(); + + return true; + } +} diff --git a/application/Espo/Core/Utils/Address/CountryDataProvider.php b/application/Espo/Core/Utils/Address/CountryDataProvider.php new file mode 100644 index 0000000000..a1ef4db927 --- /dev/null +++ b/application/Espo/Core/Utils/Address/CountryDataProvider.php @@ -0,0 +1,117 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Utils\Address; + +use Espo\Core\Utils\Config; +use Espo\Core\Utils\DataCache; +use Espo\Entities\AddressCountry; +use Espo\ORM\EntityManager; +use Espo\ORM\Query\Part\Order; + +class CountryDataProvider +{ + /** @var ?array{list: string[], preferredList: string[]} */ + private ?array $data = null; + private bool $useCache; + + private const CACHE_KEY = 'addressCountryData'; + private const LIMIT = 500; + + public function __construct( + private DataCache $dataCache, + private Config $config, + private EntityManager $entityManager + ) { + $this->useCache = (bool) $this->config->get('useCache'); + } + + /** + * @return array{list: string[], preferredList: string[]} + */ + public function get(): array + { + if ($this->data === null) { + $this->data = $this->load(); + } + + return $this->data; + } + + /** + * @return array{list: string[], preferredList: string[]} + */ + private function load(): array + { + if ($this->useCache && $this->dataCache->has(self::CACHE_KEY)) { + $list = $this->dataCache->get(self::CACHE_KEY); + + if ( + is_array($list) || + is_array($list['list'] ?? null) || + is_array($list['preferredList'] ?? null) + ) { + /** @var array{list: string[], preferredList: string[]} */ + return $list; + } + } + + $list = []; + $preferredList = []; + + /** @var iterable $collection */ + $collection = $this->entityManager + ->getRDBRepositoryByClass(AddressCountry::class) + ->sth() + ->select(['name', 'isPreferred']) + ->order('name', Order::ASC) + ->limit(0, self::LIMIT) + ->find(); + + foreach ($collection as $entity) { + $list[] = $entity->getName(); + + if ($entity->isPreferred()) { + $preferredList[] = $entity->getName(); + } + } + + if ($this->useCache) { + $this->dataCache->store(self::CACHE_KEY, [ + 'list' => $list, + 'preferredList' => $preferredList, + ]); + } + + return [ + 'list' => $list, + 'preferredList' => $preferredList, + ]; + } +} diff --git a/application/Espo/Entities/AddressCountry.php b/application/Espo/Entities/AddressCountry.php new file mode 100644 index 0000000000..ced4b74d76 --- /dev/null +++ b/application/Espo/Entities/AddressCountry.php @@ -0,0 +1,52 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Entities; + +use Espo\Core\ORM\Entity; + +class AddressCountry extends Entity +{ + public const ENTITY_TYPE = 'AddressCountry'; + + public function getName(): string + { + return $this->get('name'); + } + + public function getCode(): string + { + return $this->get('code'); + } + + public function isPreferred(): bool + { + return (bool) $this->get('isPreferred'); + } +} diff --git a/application/Espo/Hooks/AddressCountry/ClearCache.php b/application/Espo/Hooks/AddressCountry/ClearCache.php new file mode 100644 index 0000000000..c21fab3919 --- /dev/null +++ b/application/Espo/Hooks/AddressCountry/ClearCache.php @@ -0,0 +1,61 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Hooks\AddressCountry; + +use Espo\Core\Hook\Hook\AfterRemove; +use Espo\Core\Hook\Hook\AfterSave; +use Espo\Core\Utils\DataCache; +use Espo\Entities\AddressCountry; +use Espo\ORM\Entity; +use Espo\ORM\Repository\Option\RemoveOptions; +use Espo\ORM\Repository\Option\SaveOptions; + +/** + * @implements AfterRemove + * @implements AfterSave + */ +class ClearCache implements AfterRemove, AfterSave +{ + private const CACHE_KEY = 'addressCountryData'; + + public function __construct( + private DataCache $dataCache, + ) {} + + public function afterSave(Entity $entity, SaveOptions $options): void + { + $this->dataCache->clear(self::CACHE_KEY); + } + + public function afterRemove(Entity $entity, RemoveOptions $options): void + { + $this->dataCache->clear(self::CACHE_KEY); + } +} diff --git a/application/Espo/Resources/data/locale/en_US/countryList.json b/application/Espo/Resources/data/locale/en_US/countryList.json new file mode 100644 index 0000000000..2d04a99a96 --- /dev/null +++ b/application/Espo/Resources/data/locale/en_US/countryList.json @@ -0,0 +1,1006 @@ +[ + { + "name": "Afghanistan", + "code": "AF" + }, + { + "name": "Albania", + "code": "AL" + }, + { + "name": "Algeria", + "code": "DZ" + }, + { + "name": "American Samoa", + "code": "AS" + }, + { + "name": "Andorra", + "code": "AD" + }, + { + "name": "Angola", + "code": "AO" + }, + { + "name": "Anguilla", + "code": "AI" + }, + { + "name": "Antarctica", + "code": "AQ" + }, + { + "name": "Antigua and Barbuda", + "code": "AG" + }, + { + "name": "Argentina", + "code": "AR" + }, + { + "name": "Armenia", + "code": "AM" + }, + { + "name": "Aruba", + "code": "AW" + }, + { + "name": "Australia", + "code": "AU" + }, + { + "name": "Austria", + "code": "AT", + "isPreferred": true + }, + { + "name": "Azerbaijan", + "code": "AZ" + }, + { + "name": "Bahamas", + "code": "BS" + }, + { + "name": "Bahrain", + "code": "BH" + }, + { + "name": "Bangladesh", + "code": "BD" + }, + { + "name": "Barbados", + "code": "BB" + }, + { + "name": "Belarus", + "code": "BY" + }, + { + "name": "Belgium", + "code": "BE", + "isPreferred": true + }, + { + "name": "Belize", + "code": "BZ" + }, + { + "name": "Benin", + "code": "BJ" + }, + { + "name": "Bermuda", + "code": "BM" + }, + { + "name": "Bhutan", + "code": "BT" + }, + { + "name": "Bolivia", + "code": "BO" + }, + { + "name": "Bonaire", + "code": "BQ" + }, + { + "name": "Bosnia and Herzegovina", + "code": "BA" + }, + { + "name": "Botswana", + "code": "BW" + }, + { + "name": "Bouvet Island", + "code": "BV" + }, + { + "name": "Brazil", + "code": "BR" + }, + { + "name": "British Indian Ocean Territory", + "code": "IO" + }, + { + "name": "Brunei Darussalam", + "code": "BN" + }, + { + "name": "Bulgaria", + "code": "BG" + }, + { + "name": "Burkina Faso", + "code": "BF" + }, + { + "name": "Burundi", + "code": "BI" + }, + { + "name": "Cambodia", + "code": "KH" + }, + { + "name": "Cameroon", + "code": "CM" + }, + { + "name": "Canada", + "code": "CA", + "isPreferred": true + }, + { + "name": "Cape Verde", + "code": "CV" + }, + { + "name": "Cayman Islands", + "code": "KY" + }, + { + "name": "Central African Republic", + "code": "CF" + }, + { + "name": "Chad", + "code": "TD" + }, + { + "name": "Chile", + "code": "CL" + }, + { + "name": "China", + "code": "CN" + }, + { + "name": "Christmas Island", + "code": "CX" + }, + { + "name": "Cocos (Keeling) Islands", + "code": "CC" + }, + { + "name": "Colombia", + "code": "CO" + }, + { + "name": "Comoros", + "code": "KM" + }, + { + "name": "Congo", + "code": "CG" + }, + { + "name": "Congo, Democratic Republic", + "code": "CD" + }, + { + "name": "Cook Islands", + "code": "CK" + }, + { + "name": "Costa Rica", + "code": "CR" + }, + { + "name": "Croatia", + "code": "HR" + }, + { + "name": "Cuba", + "code": "CU" + }, + { + "name": "Curaçao", + "code": "CW" + }, + { + "name": "Cyprus", + "code": "CY" + }, + { + "name": "Czech Republic", + "code": "CZ" + }, + { + "name": "Côte d'Ivoire", + "code": "CI" + }, + { + "name": "Denmark", + "code": "DK", + "isPreferred": true + }, + { + "name": "Djibouti", + "code": "DJ" + }, + { + "name": "Dominica", + "code": "DM" + }, + { + "name": "Dominican Republic", + "code": "DO" + }, + { + "name": "Ecuador", + "code": "EC" + }, + { + "name": "Egypt", + "code": "EG" + }, + { + "name": "El Salvador", + "code": "SV" + }, + { + "name": "Equatorial Guinea", + "code": "GQ" + }, + { + "name": "Eritrea", + "code": "ER" + }, + { + "name": "Estonia", + "code": "EE" + }, + { + "name": "Ethiopia", + "code": "ET" + }, + { + "name": "Falkland Islands", + "code": "FK" + }, + { + "name": "Faroe Islands", + "code": "FO" + }, + { + "name": "Fiji", + "code": "FJ" + }, + { + "name": "Finland", + "code": "FI", + "isPreferred": true + }, + { + "name": "France", + "code": "FR", + "isPreferred": true + }, + { + "name": "French Guiana", + "code": "GF" + }, + { + "name": "French Polynesia", + "code": "PF" + }, + { + "name": "French Southern Territories", + "code": "TF" + }, + { + "name": "Gabon", + "code": "GA" + }, + { + "name": "Gambia", + "code": "GM" + }, + { + "name": "Georgia", + "code": "GE" + }, + { + "name": "Germany", + "code": "DE", + "isPreferred": true + }, + { + "name": "Ghana", + "code": "GH" + }, + { + "name": "Gibraltar", + "code": "GI" + }, + { + "name": "Greece", + "code": "GR" + }, + { + "name": "Greenland", + "code": "GL" + }, + { + "name": "Grenada", + "code": "GD" + }, + { + "name": "Guadeloupe", + "code": "GP" + }, + { + "name": "Guam", + "code": "GU" + }, + { + "name": "Guatemala", + "code": "GT" + }, + { + "name": "Guernsey", + "code": "GG" + }, + { + "name": "Guinea", + "code": "GN" + }, + { + "name": "Guinea-Bissau", + "code": "GW" + }, + { + "name": "Guyana", + "code": "GY" + }, + { + "name": "Haiti", + "code": "HT" + }, + { + "name": "Heard Island and McDonald Islands", + "code": "HM" + }, + { + "name": "Holy See", + "code": "VA" + }, + { + "name": "Honduras", + "code": "HN" + }, + { + "name": "Hong Kong", + "code": "HK" + }, + { + "name": "Hungary", + "code": "HU" + }, + { + "name": "Iceland", + "code": "IS" + }, + { + "name": "India", + "code": "IN" + }, + { + "name": "Indonesia", + "code": "ID" + }, + { + "name": "Iran", + "code": "IR" + }, + { + "name": "Iraq", + "code": "IQ" + }, + { + "name": "Ireland", + "code": "IE", + "isPreferred": true + }, + { + "name": "Isle of Man", + "code": "IM" + }, + { + "name": "Israel", + "code": "IL" + }, + { + "name": "Italy", + "code": "IT", + "isPreferred": true + }, + { + "name": "Jamaica", + "code": "JM" + }, + { + "name": "Japan", + "code": "JP" + }, + { + "name": "Jersey", + "code": "JE" + }, + { + "name": "Jordan", + "code": "JO" + }, + { + "name": "Kazakhstan", + "code": "KZ" + }, + { + "name": "Kenya", + "code": "KE" + }, + { + "name": "Kiribati", + "code": "KI" + }, + { + "name": "Korea, Democratic People's Republic of", + "code": "KP" + }, + { + "name": "Korea, Republic of", + "code": "KR" + }, + { + "name": "Kuwait", + "code": "KW" + }, + { + "name": "Kyrgyzstan", + "code": "KG" + }, + { + "name": "Laos", + "code": "LA" + }, + { + "name": "Latvia", + "code": "LV" + }, + { + "name": "Lebanon", + "code": "LB" + }, + { + "name": "Lesotho", + "code": "LS" + }, + { + "name": "Liberia", + "code": "LR" + }, + { + "name": "Libya", + "code": "LY" + }, + { + "name": "Liechtenstein", + "code": "LI" + }, + { + "name": "Lithuania", + "code": "LT" + }, + { + "name": "Luxembourg", + "code": "LU" + }, + { + "name": "Macao", + "code": "MO" + }, + { + "name": "North Macedonia", + "code": "MK" + }, + { + "name": "Madagascar", + "code": "MG" + }, + { + "name": "Malawi", + "code": "MW" + }, + { + "name": "Malaysia", + "code": "MY" + }, + { + "name": "Maldives", + "code": "MV" + }, + { + "name": "Mali", + "code": "ML" + }, + { + "name": "Malta", + "code": "MT" + }, + { + "name": "Marshall Islands", + "code": "MH" + }, + { + "name": "Martinique", + "code": "MQ" + }, + { + "name": "Mauritania", + "code": "MR" + }, + { + "name": "Mauritius", + "code": "MU" + }, + { + "name": "Mayotte", + "code": "YT" + }, + { + "name": "Mexico", + "code": "MX" + }, + { + "name": "Micronesia", + "code": "FM" + }, + { + "name": "Moldova", + "code": "MD" + }, + { + "name": "Monaco", + "code": "MC" + }, + { + "name": "Mongolia", + "code": "MN" + }, + { + "name": "Montenegro", + "code": "ME" + }, + { + "name": "Montserrat", + "code": "MS" + }, + { + "name": "Morocco", + "code": "MA" + }, + { + "name": "Mozambique", + "code": "MZ" + }, + { + "name": "Myanmar", + "code": "MM" + }, + { + "name": "Namibia", + "code": "NA" + }, + { + "name": "Nauru", + "code": "NR" + }, + { + "name": "Nepal", + "code": "NP" + }, + { + "name": "Netherlands", + "code": "NL", + "isPreferred": true + }, + { + "name": "New Caledonia", + "code": "NC" + }, + { + "name": "New Zealand", + "code": "NZ" + }, + { + "name": "Nicaragua", + "code": "NI" + }, + { + "name": "Niger", + "code": "NE" + }, + { + "name": "Nigeria", + "code": "NG" + }, + { + "name": "Niue", + "code": "NU" + }, + { + "name": "Norfolk Island", + "code": "NF" + }, + { + "name": "Northern Mariana Islands", + "code": "MP" + }, + { + "name": "Norway", + "code": "NO", + "isPreferred": true + }, + { + "name": "Oman", + "code": "OM" + }, + { + "name": "Pakistan", + "code": "PK" + }, + { + "name": "Palau", + "code": "PW" + }, + { + "name": "Palestine", + "code": "PS" + }, + { + "name": "Panama", + "code": "PA" + }, + { + "name": "Papua New Guinea", + "code": "PG" + }, + { + "name": "Paraguay", + "code": "PY" + }, + { + "name": "Peru", + "code": "PE" + }, + { + "name": "Philippines", + "code": "PH" + }, + { + "name": "Pitcairn", + "code": "PN" + }, + { + "name": "Poland", + "code": "PL" + }, + { + "name": "Portugal", + "code": "PT" + }, + { + "name": "Puerto Rico", + "code": "PR" + }, + { + "name": "Qatar", + "code": "QA" + }, + { + "name": "Romania", + "code": "RO" + }, + { + "name": "Russian Federation", + "code": "RU" + }, + { + "name": "Rwanda", + "code": "RW" + }, + { + "name": "Réunion", + "code": "RE" + }, + { + "name": "Saint Barthélemy", + "code": "BL" + }, + { + "name": "Saint Helena", + "code": "SH" + }, + { + "name": "Saint Kitts and Nevis", + "code": "KN" + }, + { + "name": "Saint Lucia", + "code": "LC" + }, + { + "name": "Saint Martin", + "code": "MF" + }, + { + "name": "Saint Pierre and Miquelon", + "code": "PM" + }, + { + "name": "Saint Vincent and the Grenadines", + "code": "VC" + }, + { + "name": "Samoa", + "code": "WS" + }, + { + "name": "San Marino", + "code": "SM" + }, + { + "name": "Sao Tome and Principe", + "code": "ST" + }, + { + "name": "Saudi Arabia", + "code": "SA" + }, + { + "name": "Senegal", + "code": "SN" + }, + { + "name": "Serbia", + "code": "RS" + }, + { + "name": "Seychelles", + "code": "SC" + }, + { + "name": "Sierra Leone", + "code": "SL" + }, + { + "name": "Singapore", + "code": "SG" + }, + { + "name": "Sint Maarten", + "code": "SX" + }, + { + "name": "Slovakia", + "code": "SK" + }, + { + "name": "Slovenia", + "code": "SI" + }, + { + "name": "Solomon Islands", + "code": "SB" + }, + { + "name": "Somalia", + "code": "SO" + }, + { + "name": "South Africa", + "code": "ZA" + }, + { + "name": "South Georgia and the South Sandwich Islands", + "code": "GS" + }, + { + "name": "South Sudan", + "code": "SS" + }, + { + "name": "Spain", + "code": "ES", + "isPreferred": true + }, + { + "name": "Sri Lanka", + "code": "LK" + }, + { + "name": "Sudan", + "code": "SD" + }, + { + "name": "Suriname", + "code": "SR" + }, + { + "name": "Swaziland", + "code": "SZ" + }, + { + "name": "Sweden", + "code": "SE", + "isPreferred": true + }, + { + "name": "Switzerland", + "code": "CH", + "isPreferred": true + }, + { + "name": "Syrian Arab Republic", + "code": "SY" + }, + { + "name": "Taiwan", + "code": "TW" + }, + { + "name": "Tajikistan", + "code": "TJ" + }, + { + "name": "Tanzania", + "code": "TZ" + }, + { + "name": "Thailand", + "code": "TH" + }, + { + "name": "Timor-Leste", + "code": "TL" + }, + { + "name": "Togo", + "code": "TG" + }, + { + "name": "Tokelau", + "code": "TK" + }, + { + "name": "Tonga", + "code": "TO" + }, + { + "name": "Trinidad and Tobago", + "code": "TT" + }, + { + "name": "Tunisia", + "code": "TN" + }, + { + "name": "Turkey", + "code": "TR" + }, + { + "name": "Turkmenistan", + "code": "TM" + }, + { + "name": "Turks and Caicos Islands", + "code": "TC" + }, + { + "name": "Tuvalu", + "code": "TV" + }, + { + "name": "Uganda", + "code": "UG" + }, + { + "name": "Ukraine", + "code": "UA" + }, + { + "name": "United Arab Emirates", + "code": "AE" + }, + { + "name": "United Kingdom", + "code": "GB", + "isPreferred": true + }, + { + "name": "United States", + "code": "US", + "isPreferred": true + }, + { + "name": "United States Minor Outlying Islands", + "code": "UM" + }, + { + "name": "Uruguay", + "code": "UY" + }, + { + "name": "Uzbekistan", + "code": "UZ" + }, + { + "name": "Vanuatu", + "code": "VU" + }, + { + "name": "Venezuela", + "code": "VE" + }, + { + "name": "Viet Nam", + "code": "VN" + }, + { + "name": "British Virgin Islands", + "code": "VG" + }, + { + "name": "United States Virgin Islands", + "code": "VI" + }, + { + "name": "Wallis and Futuna", + "code": "WF" + }, + { + "name": "Western Sahara", + "code": "EH" + }, + { + "name": "Yemen", + "code": "YE" + }, + { + "name": "Zambia", + "code": "ZM" + }, + { + "name": "Zimbabwe", + "code": "ZW" + } +] diff --git a/application/Espo/Resources/i18n/en_US/AddressCountry.json b/application/Espo/Resources/i18n/en_US/AddressCountry.json new file mode 100644 index 0000000000..ad41381de0 --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/AddressCountry.json @@ -0,0 +1,20 @@ +{ + "labels": { + "Create AddressCountry": "Create Address Country", + "Populate": "Populate" + }, + "fields": { + "code": "Code", + "isPreferred": "Is Preferred" + }, + "tooltips": { + "code": "ISO 3166-1 alpha-2 code.", + "isPreferred": "Preferred counties appear first in the picklist." + }, + "messages": { + "confirmPopulateDefaults": "All existing countries will be deleted, the default country list will be created. It won't be possible to revert the operation.\n\nAre you sure?" + }, + "strings": { + "populateDefaults": "Populate with default country list" + } +} diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 1c9225732d..df9cc92fcc 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -80,6 +80,7 @@ "Working Time Calendars": "Working Time Calendars", "Group Email Folders": "Group Email Folders", "Authentication Providers": "Authentication Providers", + "Address Countries": "Address Countries", "Success": "Success", "Fail": "Fail", "Configuration Instructions": "Configuration Instructions", @@ -309,7 +310,8 @@ "jobsSettings": "Job processing settings. Jobs execute tasks in the background.", "sms": "SMS settings.", "pdfTemplates": "Templates for printing to PDF.", - "formulaSandbox": "Write and test formula scripts." + "formulaSandbox": "Write and test formula scripts.", + "addressCountries": "Countries available for address fields." }, "keywords": { "settings": "system", diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 5b7c1ff8e0..6bd7041ab6 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -62,7 +62,8 @@ "WorkingTimeCalendar": "Working Time Calendar", "WorkingTimeRange": "Working Time Exception", "AuthenticationProvider": "Authentication Provider", - "GlobalStream": "Global Stream" + "GlobalStream": "Global Stream", + "AddressCountry": "Address Country" }, "scopeNamesPlural": { "Note": "Notes", @@ -115,7 +116,8 @@ "WorkingTimeCalendar": "Working Time Calendars", "WorkingTimeRange": "Working Time Exceptions", "AuthenticationProvider": "Authentication Providers", - "GlobalStream": "Global Stream" + "GlobalStream": "Global Stream", + "AddressCountry": "Address Countries" }, "labels": { "Previous Page": "Previous Page", diff --git a/application/Espo/Resources/layouts/AddressCountry/detail.json b/application/Espo/Resources/layouts/AddressCountry/detail.json new file mode 100644 index 0000000000..f2b2d52333 --- /dev/null +++ b/application/Espo/Resources/layouts/AddressCountry/detail.json @@ -0,0 +1,14 @@ +[ + { + "rows": [ + [ + {"name": "name"}, + {"name": "code"} + ], + [ + {"name": "isPreferred"}, + false + ] + ] + } +] diff --git a/application/Espo/Resources/layouts/AddressCountry/detailSmall.json b/application/Espo/Resources/layouts/AddressCountry/detailSmall.json new file mode 100644 index 0000000000..f2b2d52333 --- /dev/null +++ b/application/Espo/Resources/layouts/AddressCountry/detailSmall.json @@ -0,0 +1,14 @@ +[ + { + "rows": [ + [ + {"name": "name"}, + {"name": "code"} + ], + [ + {"name": "isPreferred"}, + false + ] + ] + } +] diff --git a/application/Espo/Resources/layouts/AddressCountry/filters.json b/application/Espo/Resources/layouts/AddressCountry/filters.json new file mode 100644 index 0000000000..c77351b0da --- /dev/null +++ b/application/Espo/Resources/layouts/AddressCountry/filters.json @@ -0,0 +1,4 @@ +[ + "code", + "isPreferred" +] diff --git a/application/Espo/Resources/layouts/AddressCountry/list.json b/application/Espo/Resources/layouts/AddressCountry/list.json new file mode 100644 index 0000000000..613bb7118b --- /dev/null +++ b/application/Espo/Resources/layouts/AddressCountry/list.json @@ -0,0 +1,5 @@ +[ + {"name": "name", "link": true}, + {"name": "code", "width": 20}, + {"name": "isPreferred", "width": 15} +] diff --git a/application/Espo/Resources/layouts/AddressCountry/massUpdate.json b/application/Espo/Resources/layouts/AddressCountry/massUpdate.json new file mode 100644 index 0000000000..2d2345e914 --- /dev/null +++ b/application/Espo/Resources/layouts/AddressCountry/massUpdate.json @@ -0,0 +1,3 @@ +[ + "isPreferred" +] diff --git a/application/Espo/Resources/metadata/app/adminPanel.json b/application/Espo/Resources/metadata/app/adminPanel.json index 34eebf13b2..e3edc69e2f 100644 --- a/application/Espo/Resources/metadata/app/adminPanel.json +++ b/application/Espo/Resources/metadata/app/adminPanel.json @@ -290,6 +290,12 @@ "iconClass": "fas fa-share-alt icon-rotate-90", "description": "webhooks" }, + { + "url": "#Admin/addressCountries", + "label": "Address Countries", + "iconClass": "far fa-flag", + "description": "addressCountries" + }, { "url": "#Admin/authenticationProviders", "label": "Authentication Providers", diff --git a/application/Espo/Resources/metadata/app/appParams.json b/application/Espo/Resources/metadata/app/appParams.json index 21f013ea74..1f5609ee5c 100644 --- a/application/Espo/Resources/metadata/app/appParams.json +++ b/application/Espo/Resources/metadata/app/appParams.json @@ -4,5 +4,8 @@ }, "extensions": { "className": "Espo\\Classes\\AppParams\\Extensions" + }, + "addressCountryData": { + "className": "Espo\\Classes\\AppParams\\AddressCountryData" } } diff --git a/application/Espo/Resources/metadata/clientDefs/AddressCountry.json b/application/Espo/Resources/metadata/clientDefs/AddressCountry.json new file mode 100644 index 0000000000..3b1af94db7 --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/AddressCountry.json @@ -0,0 +1,17 @@ +{ + "controller": "controllers/record", + "duplicateDisabled": true, + "mergeDisabled": true, + "menu": { + "list": { + "dropdown": [ + { + "name": "populateDefaults", + "labelTranslation": "AddressCountry.strings.populateDefaults", + "handler": "handlers/admin/address-country/populate-defaults", + "actionFunction": "populate" + } + ] + } + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/AddressCountry.json b/application/Espo/Resources/metadata/entityDefs/AddressCountry.json new file mode 100644 index 0000000000..68cc4864ae --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/AddressCountry.json @@ -0,0 +1,293 @@ +{ + "fields": { + "name": { + "type": "varchar", + "required": true, + "maxLength": 100 + }, + "code": { + "type": "varchar", + "required": true, + "maxLength": 2, + "tooltip": true, + "pattern": "[A-Z]{2}", + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\StringUpperCase" + ], + "options": [ + "AF", + "AX", + "AL", + "DZ", + "AS", + "AD", + "AO", + "AI", + "AQ", + "AG", + "AR", + "AM", + "AW", + "AU", + "AT", + "AZ", + "BS", + "BH", + "BD", + "BB", + "BY", + "BE", + "BZ", + "BJ", + "BM", + "BT", + "BO", + "BA", + "BW", + "BV", + "BR", + "IO", + "BN", + "BG", + "BF", + "BI", + "KH", + "CM", + "CA", + "CV", + "KY", + "CF", + "TD", + "CL", + "CN", + "CX", + "CC", + "CO", + "KM", + "CG", + "CD", + "CK", + "CR", + "CI", + "HR", + "CU", + "CY", + "CZ", + "DK", + "DJ", + "DM", + "DO", + "EC", + "EG", + "SV", + "GQ", + "ER", + "EE", + "ET", + "FK", + "FO", + "FJ", + "FI", + "FR", + "GF", + "PF", + "TF", + "GA", + "GM", + "GE", + "DE", + "GH", + "GI", + "GR", + "GL", + "GD", + "GP", + "GU", + "GT", + "GG", + "GN", + "GW", + "GY", + "HT", + "HM", + "VA", + "HN", + "HK", + "HU", + "IS", + "IN", + "ID", + "IR", + "IQ", + "IE", + "IM", + "IL", + "IT", + "JM", + "JP", + "JE", + "JO", + "KZ", + "KE", + "KI", + "KR", + "KW", + "KG", + "LA", + "LV", + "LB", + "LS", + "LR", + "LY", + "LI", + "LT", + "LU", + "MO", + "MK", + "MG", + "MW", + "MY", + "MV", + "ML", + "MT", + "MH", + "MQ", + "MR", + "MU", + "YT", + "MX", + "FM", + "MD", + "MC", + "MN", + "ME", + "MS", + "MA", + "MZ", + "MM", + "NA", + "NR", + "NP", + "NL", + "AN", + "NC", + "NZ", + "NI", + "NE", + "NG", + "NU", + "NF", + "MP", + "NO", + "OM", + "PK", + "PW", + "PS", + "PA", + "PG", + "PY", + "PE", + "PH", + "PN", + "PL", + "PT", + "PR", + "QA", + "RE", + "RO", + "RU", + "RW", + "BL", + "SH", + "KN", + "LC", + "MF", + "PM", + "VC", + "WS", + "SM", + "ST", + "SA", + "SN", + "RS", + "SC", + "SL", + "SG", + "SK", + "SI", + "SB", + "SO", + "ZA", + "GS", + "ES", + "LK", + "SD", + "SR", + "SJ", + "SZ", + "SE", + "CH", + "SY", + "TW", + "TJ", + "TZ", + "TH", + "TL", + "TG", + "TK", + "TO", + "TT", + "TN", + "TR", + "TM", + "TC", + "TV", + "UG", + "UA", + "AE", + "GB", + "US", + "UM", + "UY", + "UZ", + "VU", + "VE", + "VN", + "VG", + "VI", + "WF", + "EH", + "YE", + "ZM", + "ZW" + ] + }, + "isPreferred": { + "type": "bool", + "tooltip": true + }, + "preferredName": { + "type": "base", + "notStorable": true, + "utility": true + } + }, + "links": {}, + "collection": { + "orderBy": "preferredName", + "order": "asc", + "textFilterFields": [ + "name", + "code" + ] + }, + "indexes": { + "name": { + "unique": true, + "columns": [ + "name" + ] + } + }, + "noDeletedAttribute": true +} diff --git a/application/Espo/Resources/metadata/recordDefs/AddressCountry.json b/application/Espo/Resources/metadata/recordDefs/AddressCountry.json new file mode 100644 index 0000000000..86247f1b6b --- /dev/null +++ b/application/Espo/Resources/metadata/recordDefs/AddressCountry.json @@ -0,0 +1,11 @@ +{ + "duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General", + "massActions": { + "delete": { + "allowed": true + }, + "update": { + "allowed": true + } + } +} diff --git a/application/Espo/Resources/metadata/scopes/AddressCountry.json b/application/Espo/Resources/metadata/scopes/AddressCountry.json new file mode 100644 index 0000000000..9340e0e68d --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/AddressCountry.json @@ -0,0 +1,9 @@ +{ + "entity": true, + "importable": true, + "exportFormatList": ["csv"], + "duplicateCheckFieldList": [ + "name", + "code" + ] +} diff --git a/application/Espo/Resources/metadata/selectDefs/AddressCountry.json b/application/Espo/Resources/metadata/selectDefs/AddressCountry.json new file mode 100644 index 0000000000..91ad139339 --- /dev/null +++ b/application/Espo/Resources/metadata/selectDefs/AddressCountry.json @@ -0,0 +1,5 @@ +{ + "ordererClassNameMap": { + "preferredName": "Espo\\Classes\\Select\\AddressCountry\\PreferredNameOrderer" + } +} diff --git a/application/Espo/Tools/Address/CountryDefaultsPopulator.php b/application/Espo/Tools/Address/CountryDefaultsPopulator.php new file mode 100644 index 0000000000..829f7ff60b --- /dev/null +++ b/application/Espo/Tools/Address/CountryDefaultsPopulator.php @@ -0,0 +1,105 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\Address; + +use Espo\Core\Utils\DataCache; +use Espo\Core\Utils\File\Manager; +use Espo\Core\Utils\Id\RecordIdGenerator; +use Espo\Core\Utils\Json; +use Espo\Entities\AddressCountry; +use Espo\ORM\EntityManager; +use Espo\ORM\Query\DeleteBuilder; +use RuntimeException; + +class CountryDefaultsPopulator +{ + private string $file = 'application/Espo/Resources/data/locale/en_US/countryList.json'; + + private const CACHE_KEY = 'addressCountryData'; + + public function __construct( + private Manager $fileManager, + private EntityManager $entityManager, + private DataCache $dataCache, + private RecordIdGenerator $recordIdGenerator + ) {} + + public function populate(): void + { + if (!$this->fileManager->exists($this->file)) { + throw new RuntimeException("No file '$this->file'."); + } + + $contents = $this->fileManager->getContents($this->file); + + $dataList = Json::decode($contents, true); + + if (!is_array($dataList)) { + throw new RuntimeException("Bad data."); + } + + $collection = $this->entityManager->getCollectionFactory()->create(AddressCountry::ENTITY_TYPE); + + foreach ($dataList as $data) { + if (!is_array($data)) { + throw new RuntimeException("Bad data."); + } + + $name = $data['name'] ?? null; + $code = $data['code'] ?? null; + $isPreferred = $data['isPreferred'] ?? false; + + if (!is_string($name) || !is_string($code)) { + throw new RuntimeException("Bad data."); + } + + $entity = $this->entityManager->getNewEntity(AddressCountry::ENTITY_TYPE); + + $entity->setMultiple([ + 'id' => $this->recordIdGenerator->generate(), + 'name' => $name, + 'code' => $code, + 'isPreferred' => $isPreferred, + ]); + + $collection->append($entity); + } + + $this->entityManager->getQueryExecutor()->execute( + DeleteBuilder::create() + ->from(AddressCountry::ENTITY_TYPE) + ->build() + ); + + $this->entityManager->getMapper()->massInsert($collection); + + $this->dataCache->clear(self::CACHE_KEY); + } +} diff --git a/client/src/controllers/admin.js b/client/src/controllers/admin.js index 8ec31097b4..fb3812469c 100644 --- a/client/src/controllers/admin.js +++ b/client/src/controllers/admin.js @@ -219,6 +219,11 @@ class AdminController extends Controller { this.getRouter().dispatch('AuthenticationProvider', 'list', {fromAdmin: true}); } + // noinspection JSUnusedGlobalSymbols + actionAddressCountries() { + this.getRouter().dispatch('AddressCountry', 'list', {fromAdmin: true}); + } + // noinspection JSUnusedGlobalSymbols actionEmailAddresses() { this.getRouter().dispatch('EmailAddress', 'list', {fromAdmin: true}); diff --git a/client/src/handlers/admin/address-country/populate-defaults.js b/client/src/handlers/admin/address-country/populate-defaults.js new file mode 100644 index 0000000000..14ab3dbb9f --- /dev/null +++ b/client/src/handlers/admin/address-country/populate-defaults.js @@ -0,0 +1,48 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import ActionHandler from 'action-handler'; + +class PopulateDefaultsHandler extends ActionHandler { + + async populate() { + const confirmMessage = this.view.translate('confirmPopulateDefaults', 'messages', 'AddressCountry'); + + await this.view.confirm(confirmMessage); + + Espo.Ui.notify(' ... '); + + await Espo.Ajax.postRequest('AddressCountry/action/populateDefaults'); + await this.view.collection.fetch(); + + Espo.Ui.success(this.view.translate('Done')); + } +} + +// noinspection JSUnusedGlobalSymbols +export default PopulateDefaultsHandler; diff --git a/client/src/views/fields/address-country.js b/client/src/views/fields/address-country.js index f485ba0c8f..48aa2ed1c6 100644 --- a/client/src/views/fields/address-country.js +++ b/client/src/views/fields/address-country.js @@ -31,12 +31,57 @@ import VarcharFieldView from 'views/fields/varchar'; class AddressCountryFieldView extends VarcharFieldView { setupOptions() { - let countryList = this.getConfig().get('addressCountryList') || []; + const countryList = this.getCountryList(); if (countryList.length) { this.params.options = Espo.Utils.clone(countryList); } } + + /** + * @private + * @return {string[]} + */ + getCountryList() { + const list = (this.getHelper().getAppParam('addressCountryData') || {}).list || []; + + if (list.length) { + return list; + } + + return this.getConfig().get('addressCountryList') || []; + } + + getAutocompleteLookupFunction() { + // noinspection JSUnresolvedReference + const list = (this.getHelper().getAppParam('addressCountryData') || {}).preferredList || []; + + if (!list.length) { + return undefined; + } + + const fullList = (this.params.options || []); + + return query => { + if (query.length === 0) { + const result = list.map(item => ({value: item})); + + return Promise.resolve(result); + } + + const queryLowerCase = query.toLowerCase(); + + const result = fullList + .filter(item => { + if (item.toLowerCase().indexOf(queryLowerCase) === 0) { + return item.length !== queryLowerCase.length; + } + }) + .map(item => ({value: item})); + + return Promise.resolve(result); + }; + } } export default AddressCountryFieldView; diff --git a/client/src/views/fields/address.js b/client/src/views/fields/address.js index dc1202ea16..ccc694d8eb 100644 --- a/client/src/views/fields/address.js +++ b/client/src/views/fields/address.js @@ -386,7 +386,7 @@ class AddressFieldView extends BaseFieldView { this.$city.on('change', () => this.trigger('change')); this.$country.on('change', () => this.trigger('change')); - const countryList = this.getConfig().get('addressCountryList') || []; + const countryList = this.getCountryList(); const cityList = this.getConfig().get('addressCityList') || []; const stateList = this.getConfig().get('addressStateList') || []; @@ -398,6 +398,7 @@ class AddressFieldView extends BaseFieldView { handleFocusMode: 1, focusOnSelect: true, lookup: countryList, + lookupFunction: this.getCountryAutocompleteLookupFunction(countryList), onSelect: () => this.trigger('change'), }); @@ -610,6 +611,54 @@ class AddressFieldView extends BaseFieldView { field: this.name, }, view => view.render()); } + + /** + * @private + * @return {string[]} + */ + getCountryList() { + const list = (this.getHelper().getAppParam('addressCountryData') || {}).list || []; + + if (list.length) { + return list; + } + + return this.getConfig().get('addressCountryList') || []; + } + + /** + * @private + * @param {string[]} fullList + * @return {function(string): Promise|undefined} + */ + getCountryAutocompleteLookupFunction(fullList) { + // noinspection JSUnresolvedReference + const list = (this.getHelper().getAppParam('addressCountryData') || {}).preferredList || []; + + if (!list.length) { + return undefined; + } + + return query => { + if (query.length === 0) { + const result = list.map(item => ({value: item})); + + return Promise.resolve(result); + } + + const queryLowerCase = query.toLowerCase(); + + const result = fullList + .filter(item => { + if (item.toLowerCase().indexOf(queryLowerCase) === 0) { + return item.length !== queryLowerCase.length; + } + }) + .map(item => ({value: item})); + + return Promise.resolve(result); + }; + } } export default AddressFieldView;