Initial commit
This commit is contained in:
862
app/Http/Livewire/MainPage/SkillsCardFull.php
Normal file
862
app/Http/Livewire/MainPage/SkillsCardFull.php
Normal file
@@ -0,0 +1,862 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Helpers\StringHelper;
|
||||
use App\Http\Livewire\MainPage;
|
||||
use App\Jobs\SendEmailNewTag;
|
||||
use App\Models\Category;
|
||||
use App\Models\Language;
|
||||
use App\Models\Tag;
|
||||
use App\Models\TaggableLocale;
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Component;
|
||||
use Throwable;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
class SkillsCardFull extends Component
|
||||
{
|
||||
use TaggableWithLocale;
|
||||
use WireUiActions;
|
||||
|
||||
public $label;
|
||||
public $tagsArray = [];
|
||||
public bool $tagsArrayChanged = false;
|
||||
public bool $saveDisabled = false;
|
||||
public $initTagIds = [];
|
||||
public $initTagsArray = [];
|
||||
public $initTagsArrayTranslated = [];
|
||||
public $newTagsArray;
|
||||
public $suggestions = [];
|
||||
|
||||
public $modalVisible = false;
|
||||
|
||||
public $newTag = [];
|
||||
public $newTagCategory;
|
||||
public $categoryOptions = [];
|
||||
public $categoryColor = 'gray';
|
||||
|
||||
public bool $translationPossible = true;
|
||||
public bool $translationAllowed = true;
|
||||
public bool $translationVisible = false;
|
||||
public $translationLanguages = [];
|
||||
public $selectTranslationLanguage;
|
||||
public $translationOptions = [];
|
||||
public $selectTagTranslation;
|
||||
public $inputTagTranslation = [];
|
||||
public bool $inputDisabled = true;
|
||||
public $translateRadioButton = null;
|
||||
|
||||
public bool $sessionLanguageOk = false;
|
||||
public bool $sessionLanguageIgnored = false;
|
||||
public bool $transLanguageOk = false;
|
||||
public bool $transLanguageIgnored = false;
|
||||
|
||||
protected $langDetector = null;
|
||||
protected $listeners = [
|
||||
'save',
|
||||
'cancelCreateTag',
|
||||
'refreshComponent' => '$refresh',
|
||||
'tagifyFocus',
|
||||
'tagifyBlur',
|
||||
'saveCard'=> 'save',
|
||||
];
|
||||
|
||||
protected function rules()
|
||||
{
|
||||
return [
|
||||
'newTagsArray' => 'array',
|
||||
'newTag' => 'array',
|
||||
'newTag.name' => Rule::when(
|
||||
function ($input) {
|
||||
// Check if newTag is not an empty array
|
||||
return count($input['newTag']) > 0;
|
||||
},
|
||||
array_merge(
|
||||
timebank_config('tags.name_rule'),
|
||||
timebank_config('tags.exists_in_current_locale_rule'),
|
||||
[
|
||||
'sometimes',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!$this->sessionLanguageOk && !$this->sessionLanguageIgnored) {
|
||||
$locale = app()->getLocale();
|
||||
$localeName = \Locale::getDisplayName($locale, $locale);
|
||||
$fail(
|
||||
__('Is this :locale? Please confirm here below', [
|
||||
'locale' => $localeName
|
||||
])
|
||||
);
|
||||
}
|
||||
},
|
||||
]
|
||||
),
|
||||
),
|
||||
'newTagCategory' => Rule::when(
|
||||
function ($input) {
|
||||
if (count($input['newTag']) > 0 && $this->translationVisible === true && $this->translateRadioButton == 'input') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count($input['newTag']) > 0 && $this->translationVisible === false) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
['required', 'int'],
|
||||
),
|
||||
'selectTagTranslation' => Rule::when(
|
||||
function ($input) {
|
||||
// Check if existing tag translation is selected
|
||||
return $this->translationVisible === true && $this->translateRadioButton == 'select';
|
||||
},
|
||||
['required', 'int'],
|
||||
),
|
||||
'inputTagTranslation' => 'array',
|
||||
'inputTagTranslation.name' => Rule::when(
|
||||
fn ($input) => $this->translationVisible === true && $this->translateRadioButton === 'input',
|
||||
array_merge(
|
||||
timebank_config('tags.name_rule'),
|
||||
timebank_config('tags.exists_in_current_locale_rule'),
|
||||
[
|
||||
'sometimes',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!$this->transLanguageOk && !$this->transLanguageIgnored) {
|
||||
$baseLocale = $this->selectTranslationLanguage;
|
||||
$locale = \Locale::getDisplayName($baseLocale, $baseLocale);
|
||||
$fail(__('Is this :locale? Please confirm here below', [
|
||||
'locale' => $locale
|
||||
]));
|
||||
}
|
||||
},
|
||||
function ($attribute, $value, $fail) {
|
||||
$existsInTransLationLanguage = DB::table('taggable_tags')
|
||||
->join('taggable_locales', 'taggable_tags.tag_id', '=', 'taggable_locales.taggable_tag_id')
|
||||
->where('taggable_locales.locale', $this->selectTranslationLanguage)
|
||||
->where(function ($query) use ($value) {
|
||||
$query->where('taggable_tags.name', $value)
|
||||
->orWhere('taggable_tags.normalized', $value);
|
||||
})
|
||||
->exists();
|
||||
|
||||
if ($existsInTransLationLanguage) {
|
||||
$fail(__('This tag already exists.'));
|
||||
}
|
||||
},
|
||||
]
|
||||
),
|
||||
[]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function mount($label = null)
|
||||
{
|
||||
if ($label === null) {
|
||||
$label = __('Activities or skills you offer to other ' . platform_users());
|
||||
}
|
||||
$this->label = $label;
|
||||
|
||||
$owner = getActiveProfile();
|
||||
|
||||
if (!$owner) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($owner);
|
||||
|
||||
$owner->cleanTaggables();
|
||||
|
||||
$this->checkTranslationAllowed();
|
||||
$this->checkTranslationPossible();
|
||||
|
||||
$this->getSuggestions();
|
||||
$this->getInitialTags();
|
||||
$this->getLanguageDetector();
|
||||
$this->dispatch('load');
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected function getSuggestions()
|
||||
{
|
||||
$suggestions = (new Tag())->localTagArray(app()->getLocale());
|
||||
|
||||
$this->suggestions = collect($suggestions)->map(function ($value) {
|
||||
return app()->getLocale() == 'de' ? $value : StringHelper::DutchTitleCase($value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected function getInitialTags()
|
||||
{
|
||||
$this->initTagIds = getActiveProfile()->tags()->get()->pluck('tag_id');
|
||||
|
||||
$this->initTagsArray = TaggableLocale::whereIn('taggable_tag_id', $this->initTagIds)
|
||||
->select('taggable_tag_id', 'locale', 'updated_by_user')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$translatedTags = collect((new Tag())->translateTagIdsWithContexts($this->initTagIds));
|
||||
|
||||
$tags = $translatedTags->map(function ($item, $key) {
|
||||
return [
|
||||
'original_tag_id' => $item['original_tag_id'],
|
||||
'tag_id' => $item['tag_id'],
|
||||
'value' => $item['locale'] == App::getLocale() ? $item['tag'] : $item['tag'] . ' (' . strtoupper($item['locale']) . ')',
|
||||
'readonly' => false, // Tags are never readonly so the remove button is always visible
|
||||
'class' => $item['locale'] == App::getLocale() ? '' : 'tag-foreign-locale', // Mark foreign-locale tags with a class for diagonal stripe styling
|
||||
'locale' => $item['locale'],
|
||||
'category' => $item['category'],
|
||||
'category_path' => $item['category_path'],
|
||||
'category_color' => $item['category_color'],
|
||||
'title' => $item['category_path'], // 'title' is used by Tagify script for text that shows on hover
|
||||
'style' =>
|
||||
'--tag-bg:' .
|
||||
tailwindColorToHex($item['category_color'] . '-400') .
|
||||
'; --tag-text-color:#000' . // #111827 is gray-900
|
||||
'; --tag-hover:' .
|
||||
tailwindColorToHex($item['category_color'] . '-200'), // 'style' is used by Tagify script for background color, tailwindColorToHex is a helper function in app/Helpers/StyleHelper.php
|
||||
];
|
||||
});
|
||||
|
||||
$tags = $tags->sortBy('category_color')->values();
|
||||
$this->initTagsArrayTranslated = $tags->toArray();
|
||||
$this->tagsArray = json_encode($tags->toArray());
|
||||
}
|
||||
|
||||
|
||||
public function checkSessionLanguage()
|
||||
{
|
||||
// Ensure the language detector is initialized
|
||||
$this->getLanguageDetector();
|
||||
|
||||
$detectedLanguage = $this->langDetector->detectSimple($this->newTag['name']);
|
||||
if ($detectedLanguage === session('locale')) {
|
||||
$this->sessionLanguageOk = true;
|
||||
// No need to ignore language detection when session locale is detected
|
||||
$this->sessionLanguageIgnored = false;
|
||||
} else {
|
||||
$this->sessionLanguageOk = false;
|
||||
}
|
||||
|
||||
$this->validateOnly('newTag.name');
|
||||
}
|
||||
|
||||
|
||||
public function checkTransLanguage()
|
||||
{
|
||||
// Ensure the language detector is initialized
|
||||
$this->getLanguageDetector();
|
||||
$detectedLanguage = $this->langDetector->detectSimple($this->inputTagTranslation['name']);
|
||||
|
||||
if ($detectedLanguage === $this->selectTranslationLanguage) {
|
||||
$this->transLanguageOk = true;
|
||||
// No need to ignore language detection when base locale is detected
|
||||
$this->transLanguageIgnored = false;
|
||||
} else {
|
||||
$this->transLanguageOk = false;
|
||||
}
|
||||
$this->validateOnly('inputTagTranslation.name');
|
||||
}
|
||||
|
||||
|
||||
public function checkTranslationAllowed()
|
||||
{
|
||||
// Check if translations are allowed based on config and profile type
|
||||
$allowTranslations = timebank_config('tags.allow_tag_transations_for_non_admins', false);
|
||||
$profileType = getActiveProfileType();
|
||||
|
||||
// If config is false, only admins can add translations
|
||||
if (!$allowTranslations) {
|
||||
$this->translationAllowed = ($profileType === 'admin');
|
||||
} else {
|
||||
// If config is true, all profile types can add translations
|
||||
$this->translationAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkTranslationPossible()
|
||||
{
|
||||
// Check if profile is capable to do any translations
|
||||
$countNonBaseLanguages = getActiveProfile()->languages()->where('lang_code', '!=', timebank_config('base_language'))->count();
|
||||
if ($countNonBaseLanguages === 0 && app()->getLocale() === timebank_config('base_language')) {
|
||||
$this->translationPossible = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getLanguageDetector()
|
||||
{
|
||||
if (!$this->langDetector) {
|
||||
$this->langDetector = new \Text_LanguageDetect();
|
||||
$this->langDetector->setNameMode(2); // iso language code with 2 characters
|
||||
}
|
||||
return $this->langDetector;
|
||||
}
|
||||
|
||||
|
||||
public function updatedNewTagName()
|
||||
{
|
||||
$this->resetErrorBag('newTag.name');
|
||||
|
||||
// Check if name is the profiles's session's locale
|
||||
$this->checkSessionLanguage();
|
||||
// Only fall back to initTagsArray if newTagsArray has not been set yet,
|
||||
// to preserve any tags the user already added before opening the create modal
|
||||
if ($this->newTagsArray === null) {
|
||||
$this->newTagsArray = collect($this->initTagsArray);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedSessionLanguageIgnored()
|
||||
{
|
||||
if (!$this->sessionLanguageIgnored) {
|
||||
$this->checkSessionLanguage();
|
||||
}
|
||||
|
||||
// Revalidate the newTag.name field
|
||||
$this->validateOnly('newTag.name');
|
||||
}
|
||||
|
||||
|
||||
public function updatedTransLanguageIgnored()
|
||||
{
|
||||
if (!$this->transLanguageIgnored) {
|
||||
$this->checkTransLanguage();
|
||||
} else {
|
||||
$this->resetErrorBag('inputTagTranslation.name');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updatedSelectTranslationLanguage()
|
||||
{
|
||||
$this->selectTagTranslation = [];
|
||||
// Suggest related tags in the selected translation language
|
||||
$this->translationOptions = $this->getTranslationOptions($this->selectTranslationLanguage);
|
||||
}
|
||||
|
||||
|
||||
public function updatedNewTagCategory()
|
||||
{
|
||||
$this->categoryColor = collect($this->categoryOptions)
|
||||
->firstWhere('category_id', $this->newTagCategory)['color'] ?? 'gray';
|
||||
$this->selectTagTranslation = [];
|
||||
// Suggest related tags in the selected translation language
|
||||
$this->translationOptions = $this->getTranslationOptions($this->selectTranslationLanguage);
|
||||
$this->resetErrorBag('inputTagTranslationCategory');
|
||||
}
|
||||
|
||||
|
||||
public function updatedInputTagTranslationName()
|
||||
{
|
||||
$this->validateOnly('inputTagTranslation.name');
|
||||
}
|
||||
|
||||
|
||||
public function updatedTagsArray()
|
||||
{
|
||||
// Prevent save during updating cycle of the tagsArray
|
||||
$this->saveDisabled = true;
|
||||
$this->newTagsArray = collect(json_decode($this->tagsArray, true));
|
||||
|
||||
$localesToCheck = [app()->getLocale(), '']; // Only current locale and tags without locale should be checked for any new tag keywords
|
||||
$newTagsArrayLocal = $this->newTagsArray->whereIn('locale', $localesToCheck);
|
||||
// map suggestion to lower case for search normalization of the $newEntries
|
||||
$suggestions = collect($this->suggestions)->map(function ($value) {
|
||||
return strtolower($value);
|
||||
});
|
||||
// Retrieve new tag entries not present in suggestions
|
||||
$newEntries = $newTagsArrayLocal->filter(function ($newItem) use ($suggestions) {
|
||||
return !$suggestions->contains(strtolower($newItem['value']));
|
||||
});
|
||||
// Add a new tag modal if there are new entries
|
||||
if (count($newEntries) > 0) {
|
||||
|
||||
$this->newTag['name'] = app()->getLocale() == 'de' ? $newEntries->flatten()->first() : ucfirst($newEntries->flatten()->first());
|
||||
$this->categoryOptions = Category::where('type', Tag::class)
|
||||
->get()
|
||||
->map(function ($category) {
|
||||
// Include all attributes, including appended ones
|
||||
return [
|
||||
'category_id' => $category->id,
|
||||
'name' => ucfirst($category->translation->name ?? ''), // Use the appended 'translation' attribute
|
||||
'description' => $category->relatedPathExSelfTranslation ?? '', // Appended attribute
|
||||
'color' => $category->relatedColor ?? 'gray',
|
||||
];
|
||||
})
|
||||
->sortBy('name')
|
||||
->values();
|
||||
|
||||
// Open the create tag modal
|
||||
$this->modalVisible = true;
|
||||
|
||||
// For proper validation, this needs to be done after the netTag.name input of the modal is visible
|
||||
$this->checkSessionLanguage();
|
||||
|
||||
} else {
|
||||
$newEntries = false;
|
||||
|
||||
// Enable save button as updating cycle tagsArray is finished by now
|
||||
$this->saveDisabled = false;
|
||||
}
|
||||
$this->checkChangesTagsArray();
|
||||
}
|
||||
|
||||
|
||||
public function checkChangesTagsArray()
|
||||
{
|
||||
// Check if tagsArray has been changed, to display 'unsaved changes' message next to save button
|
||||
$initTagIds = collect($this->initTagIds);
|
||||
$newTagIds = $this->newTagsArray->pluck('tag_id');
|
||||
$diffFromNew = $newTagIds->diff($initTagIds);
|
||||
$diffFromInit = $initTagIds->diff($newTagIds);
|
||||
|
||||
if ($diffFromNew->isNotEmpty() || $diffFromInit->isNotEmpty()) {
|
||||
$this->tagsArrayChanged = true;
|
||||
} else {
|
||||
$this->tagsArrayChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// When tagify raises focus, disable the save button
|
||||
public function tagifyFocus()
|
||||
{
|
||||
$this->saveDisabled = true;
|
||||
}
|
||||
|
||||
// When tagify looses focus, enable the save button
|
||||
public function tagifyBlur()
|
||||
{
|
||||
$this->saveDisabled = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function renderedModalVisible()
|
||||
{
|
||||
// Enable save button as updating cycle tagsArray is finished by now
|
||||
$this->saveDisabled = false;
|
||||
}
|
||||
|
||||
|
||||
public function updatedTranslationVisible()
|
||||
{
|
||||
if ($this->translationVisible && $this->translationAllowed) {
|
||||
$this->updatedNewTagCategory();
|
||||
|
||||
$profile = getActiveProfile();
|
||||
|
||||
if (!$profile) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
|
||||
// Get all languages of the profile with good competence
|
||||
$this->translationLanguages = $profile
|
||||
->languages()
|
||||
->wherePivot('competence', 1)
|
||||
->where('lang_code', '!=', app()->getLocale())
|
||||
->get()
|
||||
->map(function ($language) {
|
||||
$language->name = trans($language->name); // Map the name property to a translation key
|
||||
return $language;
|
||||
});
|
||||
|
||||
// Make sure that always the base language is included even if the profile does not has it as a competence 1
|
||||
if (!$this->translationLanguages->contains('lang_code', 'en')) {
|
||||
$transLanguage = Language::where('lang_code', timebank_config('base_language'))->first();
|
||||
if ($transLanguage) {
|
||||
$transLanguage->name = trans($transLanguage->name); // Map the name property to a translation key
|
||||
// Add the base language to the translationLanguages collection
|
||||
$this->translationLanguages = collect($this->translationLanguages)
|
||||
->push($transLanguage);
|
||||
}
|
||||
|
||||
// Set the default selection to base language
|
||||
if (app()->getLocale() != timebank_config('base_language')) {
|
||||
$this->selectTranslationLanguage = timebank_config('base_language');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function updatedTranslateRadioButton()
|
||||
{
|
||||
if ($this->translateRadioButton === 'select') {
|
||||
$this->inputDisabled = true;
|
||||
$this->dispatch('disableInput');
|
||||
} elseif ($this->translateRadioButton === 'input') {
|
||||
$this->inputDisabled = false;
|
||||
// $this->dispatch('disableSelect'); // Script inside view skills-form.blade.php
|
||||
}
|
||||
$this->resetErrorBag('selectTagTranslation');
|
||||
$this->resetErrorBag('inputTagTranslation.name');
|
||||
$this->resetErrorBag('newTagCategory');
|
||||
}
|
||||
|
||||
|
||||
public function updatedSelectTagTranslation()
|
||||
{
|
||||
if ($this->selectTagTranslation) {
|
||||
$this->categoryColor = Tag::find($this->selectTagTranslation)->categories->first()->relatedColor ?? 'gray';
|
||||
$this->translateRadioButton = 'select';
|
||||
$this->dispatch('disableInput');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updatedInputTagTranslation()
|
||||
{
|
||||
$this->translateRadioButton = 'input';
|
||||
$this->inputDisabled = false;
|
||||
// $this->dispatch('disableSelect'); // Script inside view skills-form.blade.php
|
||||
$this->checkTransLanguage();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the visibility of the modal. If the modal becomes invisible, dispatches the 'remove' event to remove the last value of the tags array on the front-end.
|
||||
*/
|
||||
public function updatedModalVisible()
|
||||
{
|
||||
if ($this->modalVisible == false) {
|
||||
$this->dispatch('remove'); // Removes last value of the tagsArray on front-end only
|
||||
$this->dispatch('reinitializeComponent');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a list of related tags based on the specified category and locale.
|
||||
* Get all translation options in the choosen locale,
|
||||
* but exclude all tags already have a similar context in the current $appLocal.
|
||||
*
|
||||
* @param int|null $category The ID of the category to filter related tags. If null, all tags in the locale are suggested.
|
||||
* @param string|null $locale The locale to use for tag names. If not provided, the application's current locale is used.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection A collection of tags containing 'tag_id' and 'name' keys, sorted by name.
|
||||
*/
|
||||
|
||||
public function getTranslationOptions($locale)
|
||||
{
|
||||
$appLocale = app()->getLocale();
|
||||
|
||||
// 1) Get all context_ids used by tags that match app()->getLocale()
|
||||
$contextIdsInAppLocale = DB::table('taggable_locale_context')
|
||||
->whereIn('tag_id', function ($query) use ($appLocale) {
|
||||
$query->select('taggable_tag_id')
|
||||
->from('taggable_locales')
|
||||
->where('locale', $appLocale);
|
||||
})
|
||||
->pluck('context_id');
|
||||
|
||||
// 2) Exclude tags that share these context_ids
|
||||
$tags = Tag::with(['locale', 'contexts.category'])
|
||||
->whereHas('locale', function ($query) use ($locale) {
|
||||
$query->where('locale', $locale);
|
||||
})
|
||||
->whereNotIn('tag_id', function ($subquery) use ($contextIdsInAppLocale) {
|
||||
$subquery->select('tag_id')
|
||||
->from('taggable_locale_context')
|
||||
->whereIn('context_id', $contextIdsInAppLocale);
|
||||
})
|
||||
->get();
|
||||
|
||||
// 3) Build the options array. Adjust the name logic to your preference.
|
||||
$options = $tags->map(function ($tag) use ($locale) {
|
||||
$category = optional($tag->contexts->first())->category;
|
||||
$description = optional(optional($category)->translation)->name ?? '';
|
||||
|
||||
return [
|
||||
'tag_id' => $tag->tag_id,
|
||||
'name' => $locale == 'de' ? $tag->name : StringHelper::DutchTitleCase($tag->normalized),
|
||||
'description' => $description,
|
||||
];
|
||||
})->sortBy('name')->values();
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancels the creation of a new tag by resetting error messages,
|
||||
* clearing input fields, hiding translation and modal visibility,
|
||||
* and resetting tag arrays to their initial state.
|
||||
*/
|
||||
public function cancelCreateTag()
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
$this->newTag = [];
|
||||
$this->newTagCategory = null;
|
||||
$this->translationVisible = false;
|
||||
$this->translateRadioButton = null;
|
||||
$this->sessionLanguageOk = false;
|
||||
$this->sessionLanguageIgnored = false;
|
||||
$this->transLanguageOk = false;
|
||||
$this->transLanguageIgnored = false;
|
||||
$this->categoryColor = 'gray';
|
||||
$this->selectTagTranslation = null;
|
||||
$this->inputTagTranslation = [];
|
||||
$this->inputDisabled = true;
|
||||
// $this->newTagsArray = collect($this->initTagsArray);
|
||||
// $this->tagsArray = json_encode($this->initTagsArray);
|
||||
|
||||
// Remove last value of the tagsArray
|
||||
$tagsArray = is_string($this->tagsArray) ? json_decode($this->tagsArray, true) : $this->tagsArray;
|
||||
array_pop($tagsArray);
|
||||
$this->tagsArray = json_encode($tagsArray);
|
||||
|
||||
// Check of there were also other unsaved new tags in the tagsArray
|
||||
$hasNoTagId = false;
|
||||
if (is_array($tagsArray)) {
|
||||
$this->tagsArrayChanged = count(array_filter($tagsArray, function ($tag) {
|
||||
return !array_key_exists('tag_id', $tag) || $tag['tag_id'] === null;
|
||||
})) > 0;
|
||||
} else {
|
||||
$this->tagsArrayChanged = false;
|
||||
}
|
||||
|
||||
$this->modalVisible = false;
|
||||
$this->updatedModalVisible();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handles the save button of the create tag modal.
|
||||
*
|
||||
* Creates a new tag for the currently active profile and optionally
|
||||
* associates it with a category or base-language translation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function createTag()
|
||||
{
|
||||
// TODO: MAKE A TRANSACTION
|
||||
|
||||
$this->validate();
|
||||
$this->resetErrorBag();
|
||||
|
||||
// Format strings to correct case
|
||||
$this->newTag['name'] = app()->getLocale() == 'de' ? $this->newTag['name'] : StringHelper::DutchTitleCase($this->newTag['name']);
|
||||
|
||||
$name = $this->newTag['name'];
|
||||
$normalized = call_user_func(config('taggable.normalizer'), $name);
|
||||
|
||||
// Create the tag and attach the owner and context
|
||||
$tag = Tag::create([
|
||||
'name' => $name,
|
||||
'normalized' => $normalized,
|
||||
]);
|
||||
$owner = getActiveProfile();
|
||||
|
||||
if (!$owner) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($owner);
|
||||
$owner->tagById($tag->tag_id);
|
||||
$context = [
|
||||
'category_id' => $this->newTagCategory,
|
||||
'updated_by_user' => Auth::guard('web')->user()->id, // use the logged user, not the active profile
|
||||
];
|
||||
|
||||
if ($this->translationVisible) {
|
||||
if ($this->translateRadioButton === 'select') {
|
||||
// Attach an existing context in the base language to the new tag. See timebank_config('base_language')
|
||||
// Note that the category_id and updated_by_user is not updated when selecting an existing context
|
||||
$tagContext = Tag::find($this->selectTagTranslation)
|
||||
->contexts()
|
||||
->first();
|
||||
$tag->contexts()->attach($tagContext->id);
|
||||
} elseif ($this->translateRadioButton === 'input') {
|
||||
// Create a new context for the new tag
|
||||
$tagContext = $tag->contexts()->create($context);
|
||||
|
||||
// Create a new translation of the tag
|
||||
$this->inputTagTranslation['name'] = $this->selectTranslationLanguage == 'de' ? $this->inputTagTranslation['name'] : StringHelper::DutchTitleCase($this->inputTagTranslation['name']);
|
||||
// $owner->tag($this->inputTagTranslation['name']);
|
||||
$nameTranslation = $this->inputTagTranslation['name'];
|
||||
$normalizedTranslation = call_user_func(config('taggable.normalizer'), $nameTranslation);
|
||||
$locale = ['locale' => $this->selectTranslationLanguage ];
|
||||
|
||||
// Create the translation tag with the locale and attach the context
|
||||
$tagTranslation = Tag::create([
|
||||
'name' => $nameTranslation,
|
||||
'normalized' => $normalizedTranslation,
|
||||
]);
|
||||
$tagTranslation->locale()->create($locale);
|
||||
$tagTranslation->contexts()->attach($tagContext->id);
|
||||
|
||||
// The translation now has been recorded. Next, detach owner from this translation as only the locale tag should be attached to the owner
|
||||
$owner->untagById([$tagTranslation->tag_id]);
|
||||
// Also clean up owner's tags that have similar context but have different locale. Only the tag in owner's app()->getLocale() should remain in db.
|
||||
$owner->cleanTaggables(); // In TaggableWithLocale trait
|
||||
|
||||
}
|
||||
} else {
|
||||
// Create a new context for the new tag without translation
|
||||
$tagContext = $tag->contexts()->create($context);
|
||||
}
|
||||
|
||||
$this->modalVisible = false;
|
||||
$this->saveDisabled = false;
|
||||
// Attach the new collection of tags to the active profile
|
||||
$this->save();
|
||||
$this->tagsArrayChanged = false;
|
||||
|
||||
// Dispatch the SendEmailNewTag job
|
||||
SendEmailNewTag::dispatch($tag->tag_id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves the newTagsArray: attaches the current tags to the profile model.
|
||||
* Ignores the tags that are marked read-only (no app locale and no base language locale).
|
||||
* Dispatches notification on success or error.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if ($this->saveDisabled === false) {
|
||||
|
||||
if ($this->newTagsArray) {
|
||||
try {
|
||||
// Use a transaction for saving skill tags
|
||||
DB::transaction(function () {
|
||||
// Make sure we can count newTag for conditional validation rules
|
||||
if ($this->newTag === null) {
|
||||
$this->newTag = [];
|
||||
}
|
||||
|
||||
$owner = getActiveProfile();
|
||||
|
||||
if (!$owner) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($owner);
|
||||
|
||||
$this->validate();
|
||||
// try {
|
||||
// $this->validate();
|
||||
// } catch (ValidationException $e) {
|
||||
// // Dump all validation errors to the log or screen
|
||||
// logger()->error('Validation failed', $e->errors());
|
||||
// dd($e->errors()); // or use dump() if you prefer
|
||||
// }
|
||||
$this->resetErrorBag();
|
||||
|
||||
$initTags = collect($this->initTagsArray)->pluck('taggable_tag_id');
|
||||
|
||||
$newTagsArray = collect($this->newTagsArray);
|
||||
|
||||
$newTags = $newTagsArray
|
||||
->where('tag_id', null)
|
||||
->pluck('value')->toArray();
|
||||
$owner->tag($newTags);
|
||||
|
||||
$remainingTags = $this->newTagsArray
|
||||
->where('tag_id')
|
||||
->pluck('tag_id')->toArray();
|
||||
|
||||
$removedTags = $initTags->diff($remainingTags)->toArray();
|
||||
$owner->untagById($removedTags);
|
||||
|
||||
// Finaly clean up taggables table: remove duplicate contexts and any orphaned taggables
|
||||
// In TaggableWithLocale trait
|
||||
$owner->cleanTaggables();
|
||||
|
||||
$owner->touch(); // Observer catches this and reindexes search index
|
||||
|
||||
// WireUI notification
|
||||
$this->notification()->success($title = __('Your have updated your profile successfully!'));
|
||||
});
|
||||
// end of transaction
|
||||
} catch (Throwable $e) {
|
||||
// WireUI notification
|
||||
// TODO!: create event to send error notification to admin
|
||||
$this->notification([
|
||||
'title' => __('Update failed!'),
|
||||
'description' => __('Sorry, your data could not be saved!') . '<br /><br />' . __('Our team has ben notified about this error. Please try again later.') . '<br /><br />' . $e->getMessage(),
|
||||
'icon' => 'error',
|
||||
'timeout' => 100000,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->tagsArrayChanged = false;
|
||||
$this->dispatch('saved');
|
||||
$this->forgetCachedSkills();
|
||||
$this->cacheSkills();
|
||||
$this->initTagsArray = [];
|
||||
$this->newTag = null;
|
||||
$this->newTagsArray = null;
|
||||
$this->newTagCategory = null;
|
||||
$this->dispatch('refreshComponent');
|
||||
$this->dispatch('reinitializeTagify');
|
||||
$this->dispatch('reloadPage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function forgetCachedSkills()
|
||||
{
|
||||
// Get the profile type (user / organization) from the session and convert to lowercase
|
||||
$profileType = strtolower(basename(str_replace('\\', '/', session('activeProfileType'))));
|
||||
// Get the supported locales from the config
|
||||
$locales = config('app.supported_locales', [app()->getLocale()]);
|
||||
// Iterate over each locale and forget the cache
|
||||
foreach ($locales as $locale) {
|
||||
Cache::forget('skills-' . $profileType . '-' . session('activeProfileId') . '-lang-' . $locale);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function cacheSkills()
|
||||
{
|
||||
$profileType = strtolower(basename(str_replace('\\', '/', session('activeProfileType')))); // Get the profile type (user / organization) from the session and convert to lowercase
|
||||
|
||||
$skillsCache = Cache::remember('skills-' . $profileType . '-' . session('activeProfileId') . '-lang-' . app()->getLocale(), now()->addDays(7), function () {
|
||||
// remember cache for 7 days
|
||||
$tagIds = session('activeProfileType')::find(session('activeProfileId'))->tags->pluck('tag_id');
|
||||
$translatedTags = collect((new Tag())->translateTagIdsWithContexts($tagIds, App::getLocale(), App::getFallbackLocale())); // Translate to app locale, if not available to fallback locale, if not available do not translate
|
||||
$skills = $translatedTags->map(function ($item, $key) {
|
||||
return [
|
||||
'original_tag_id' => $item['original_tag_id'],
|
||||
'tag_id' => $item['tag_id'],
|
||||
'name' => $item['tag'],
|
||||
'foreign' => $item['locale'] == App::getLocale() ? false : true, // Mark all tags in a foreign language read-only, as users need to switch locale to edit/update/etc foreign tags
|
||||
'locale' => $item['locale'],
|
||||
'category' => $item['category'],
|
||||
'category_path' => $item['category_path'],
|
||||
'category_color' => $item['category_color'],
|
||||
];
|
||||
});
|
||||
$skills = collect($skills);
|
||||
|
||||
return $skills;
|
||||
});
|
||||
|
||||
$this->tagsArray = json_encode($skillsCache->toArray());
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.skills-card-full');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user