From 2597d182dbda1adfbf3ef2f6f0a3dfb48a484260 Mon Sep 17 00:00:00 2001 From: m2cci-bartetj Date: Thu, 16 Apr 2026 11:54:12 +0200 Subject: [PATCH] FIX FLEXIAPI-441 add color picker and logo uploader to space settings --- INSTALL.md | 1 + .../Controllers/Admin/SpaceController.php | 71 ++++--- .../Controllers/Api/Admin/SpaceController.php | 5 + flexiapi/app/Space.php | 14 ++ flexiapi/app/bootstrap/cache/.gitignore | 2 + ...d_theme_color_and_logo_to_spaces_table.php | 24 +++ flexiapi/lang/fr.json | 4 +- flexiapi/public/css/form.css | 122 ++++++++++-- flexiapi/public/css/style.css | 10 +- flexiapi/public/img/logo.svg | 55 ++---- flexiapi/public/scripts/colorPicker.js | 179 ++++++++++++++++++ .../views/admin/space/configuration.blade.php | 80 ++++++-- .../views/api/documentation/spaces.blade.php | 2 + .../resources/views/layouts/main.blade.php | 17 +- flexisip-account-manager.spec | 2 + 15 files changed, 472 insertions(+), 116 deletions(-) create mode 100755 flexiapi/app/bootstrap/cache/.gitignore create mode 100644 flexiapi/database/migrations/2026_04_14_145641_add_theme_color_and_logo_to_spaces_table.php create mode 100644 flexiapi/public/scripts/colorPicker.js diff --git a/INSTALL.md b/INSTALL.md index 383a28d..353af85 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -14,6 +14,7 @@ Clone the repository, install the dependencies and generate a key. composer install --no-dev php artisan key:generate + php artisan storage:link # 1.b Packages setup diff --git a/flexiapi/app/Http/Controllers/Admin/SpaceController.php b/flexiapi/app/Http/Controllers/Admin/SpaceController.php index 9ec7e7c..4baa2d5 100644 --- a/flexiapi/app/Http/Controllers/Admin/SpaceController.php +++ b/flexiapi/app/Http/Controllers/Admin/SpaceController.php @@ -27,6 +27,7 @@ use App\Rules\Domain; use Illuminate\Http\Request; use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\Storage; class SpaceController extends Controller { @@ -64,7 +65,7 @@ class SpaceController extends Controller $request->merge(['full_host' => $fullHost]); $request->validate([ - 'host' => 'nullable|regex:/'. Space::HOST_REGEX . '/', + 'host' => 'nullable|regex:/' . Space::HOST_REGEX . '/', 'full_host' => ['required', 'unique:spaces,host', new Domain()], ]); @@ -113,7 +114,46 @@ class SpaceController extends Controller public function configurationUpdate(Request $request, Space $space) { - $space = $this->setConfiguration($request, $space); + $request->validate([ + 'newsletter_registration_address' => 'nullable|email', + 'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)], + 'logo' => ['nullable', 'image', 'mimes:png', 'max:2048'], + 'theme_hue' => 'nullable|integer|min:0|max:360', + ]); + + if ($request->logo_delete == 1 && $space->logo) { + Storage::disk('public')->delete($space->LOGO_PATH); + $space->logo = null; + $space->save(); + } + + if ($request->hasFile('logo')) { + if ($space->logo) { + Storage::disk('public')->delete($space->LOGO_PATH); + } + $filename = $request->file('logo')->hashName(); + $request->file('logo')->storeAs('img', $filename, 'public'); + $space->logo = $filename; + } + + $space->theme_hue = $request->get('theme_hue'); + $space->copyright_text = $request->get('copyright_text'); + $space->intro_registration_text = $request->get('intro_registration_text'); + $space->newsletter_registration_address = $request->get('newsletter_registration_address'); + $space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address'); + + if ($space->accounts()->count() == 0) { + $space->account_realm = $request->get('account_realm'); + } + + $space->custom_provisioning_entries = $request->get('custom_provisioning_entries'); + $space->custom_provisioning_overwrite_all = getRequestBoolean($request, 'custom_provisioning_overwrite_all'); + $space->provisioning_use_linphone_provisioning_header = getRequestBoolean($request, 'provisioning_use_linphone_provisioning_header'); + + $space->public_registration = getRequestBoolean($request, 'public_registration'); + $space->phone_registration = getRequestBoolean($request, 'phone_registration'); + $space->intercom_features = getRequestBoolean($request, 'intercom_features'); + $space->save(); return redirect()->route('admin.spaces.configuration', $space); @@ -152,33 +192,6 @@ class SpaceController extends Controller return redirect()->route('admin.spaces.show', $space); } - private function setConfiguration(Request $request, Space $space) - { - $request->validate([ - 'newsletter_registration_address' => 'nullable|email', - 'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)] - ]); - - $space->copyright_text = $request->get('copyright_text'); - $space->intro_registration_text = $request->get('intro_registration_text'); - $space->newsletter_registration_address = $request->get('newsletter_registration_address'); - $space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address'); - - if ($space->accounts()->count() == 0) { - $space->account_realm = $request->get('account_realm'); - } - - $space->custom_provisioning_entries = $request->get('custom_provisioning_entries'); - $space->custom_provisioning_overwrite_all = getRequestBoolean($request, 'custom_provisioning_overwrite_all'); - $space->provisioning_use_linphone_provisioning_header = getRequestBoolean($request, 'provisioning_use_linphone_provisioning_header'); - - $space->public_registration = getRequestBoolean($request, 'public_registration'); - $space->phone_registration = getRequestBoolean($request, 'phone_registration'); - $space->intercom_features = getRequestBoolean($request, 'intercom_features'); - - return $space; - } - private function setAppConfiguration(Request $request, Space $space) { $request->validate([ diff --git a/flexiapi/app/Http/Controllers/Api/Admin/SpaceController.php b/flexiapi/app/Http/Controllers/Api/Admin/SpaceController.php index b7fa35a..5566b07 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/SpaceController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/SpaceController.php @@ -42,6 +42,7 @@ class SpaceController extends Controller 'max_accounts' => 'nullable|integer', 'expire_at' => 'nullable|date|after_or_equal:today', 'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)], + 'theme_hue' => 'nullable|integer|min:0|max:360', ]); $space = new Space; @@ -57,6 +58,7 @@ class SpaceController extends Controller $space->max_accounts = $request->get('max_accounts', 0); $space->name = $request->get('name'); $space->newsletter_registration_address = $request->get('newsletter_registration_address'); + $space->theme_hue = $request->get('theme_hue'); $this->setRequestBoolean($request, $space, 'assistant_disable_qr_code'); $this->setRequestBoolean($request, $space, 'assistant_hide_create_account'); $this->setRequestBoolean($request, $space, 'assistant_hide_third_party_account'); @@ -76,6 +78,7 @@ class SpaceController extends Controller $this->setRequestBoolean($request, $space, 'public_registration'); $this->setRequestBoolean($request, $space, 'super'); $this->setRequestBoolean($request, $space, 'web_panel'); + $space->save(); return $space->refresh(); @@ -125,6 +128,7 @@ class SpaceController extends Controller 'public_registration' => 'required|boolean', 'super' => 'required|boolean', 'web_panel' => 'required|boolean', + 'theme_hue' => 'nullable|integer|min:0|max:360', ]); $space = Space::where('domain', $domain)->firstOrFail(); @@ -170,6 +174,7 @@ class SpaceController extends Controller $space->provisioning_use_linphone_provisioning_header = $request->get('provisioning_use_linphone_provisioning_header'); $space->public_registration = $request->get('public_registration'); $space->web_panel = $request->get('web_panel'); + $space->theme_hue = $request->get('theme_hue'); $space->save(); diff --git a/flexiapi/app/Space.php b/flexiapi/app/Space.php index c6003e0..6cf90d4 100644 --- a/flexiapi/app/Space.php +++ b/flexiapi/app/Space.php @@ -19,6 +19,7 @@ namespace App; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Auth; @@ -75,6 +76,7 @@ class Space extends Model public const HOST_REGEX = '[\w\-]+'; public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-z0-9-]{1,63}(?attributes['logo']; + } + public function isFull(): bool { return $this->max_accounts > 0 && ($this->accounts()->count() >= $this->max_accounts); @@ -197,4 +204,11 @@ class Space extends Model return null; } + + protected function themeHue(): Attribute + { + return Attribute::make( + get: fn($value) => $value ?? 22 + ); + } } diff --git a/flexiapi/app/bootstrap/cache/.gitignore b/flexiapi/app/bootstrap/cache/.gitignore new file mode 100755 index 0000000..d6b7ef3 --- /dev/null +++ b/flexiapi/app/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/flexiapi/database/migrations/2026_04_14_145641_add_theme_color_and_logo_to_spaces_table.php b/flexiapi/database/migrations/2026_04_14_145641_add_theme_color_and_logo_to_spaces_table.php new file mode 100644 index 0000000..be99bf1 --- /dev/null +++ b/flexiapi/database/migrations/2026_04_14_145641_add_theme_color_and_logo_to_spaces_table.php @@ -0,0 +1,24 @@ +integer('theme_hue')->nullable(); + $table->string('logo')->nullable(); + }); + } + + public function down(): void + { + Schema::table('spaces', function (Blueprint $table) { + $table->dropColumn('theme_hue'); + $table->dropColumn('logo'); + }); + } +}; diff --git a/flexiapi/lang/fr.json b/flexiapi/lang/fr.json index db28c1c..1144dc9 100644 --- a/flexiapi/lang/fr.json +++ b/flexiapi/lang/fr.json @@ -121,6 +121,7 @@ "Files": "Fichiers", "From": "Depuis", "Hello":"Bonjour", + "Hex" : "Hex", "Host": "Hôte", "I accept the Privacy Policy": "J'accepte la Politique de Confidentialité", "I accept the Terms and Conditions": "J'accepte les Conditions Générales", @@ -186,6 +187,7 @@ "Phone Countries": "Numéros Internationaux", "Phone number": "Numéro de téléphone", "Phone registration": "Inscription par téléphone", + "Platform customization": "Customisation de la plateforme", "Please enter the new email that you would like to link to your account.": "Veuillez entre l'adresse email que vous souhaitez lier à votre compte.", "Please enter the new phone number that you would like to link to your account.": "Veuillez entrer le numéro de téléphone que vous souhaitez lier à votre compte.", "Priority rule": "Rêgle prioritaire", @@ -305,4 +307,4 @@ "Your password" : "Votre mot de passe", "Your space :space is expiring in :count days": "Votre espace :space expire dans :count jours", "Your space has expired. Access to your interface is now disabled, and your users can no longer benefit from the service. To reactivate your space, please contact your account manager.": "Votre espace est arrivé à expiration. L’accès à votre interface est désormais désactivé, et vos utilisateurs ne peuvent plus bénéficier du service. Pour réactiver votre espace, veuillez contacter votre responsable de compte." -} \ No newline at end of file +} diff --git a/flexiapi/public/css/form.css b/flexiapi/public/css/form.css index 2a0b0fe..a006c67 100644 --- a/flexiapi/public/css/form.css +++ b/flexiapi/public/css/form.css @@ -1,5 +1,6 @@ /** Forms **/ +form div input[type="file"]::file-selector-button, .btn { display: inline-block; background-color: var(--main-5); @@ -157,7 +158,6 @@ form .disabled:not(a) { form label { color: var(--second-6); font-size: 1.5rem; - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -183,12 +183,21 @@ form div .btn.oppose { right: 0; } -form div input, +form div input:not([type=range]):not([type=button]):not([type=file]):not(.btn), form div textarea, form div select { padding: 1rem 2rem; background-color: var(--grey-1); border-radius: 3rem; + border: 0.063rem solid var(--grey-2); + font-size: 1.5rem; + resize: vertical; +} + +form div input[type=file] { + color: var(--second-6); + background-color: var(--grey-1); + border-radius: 3rem; border: 1px solid var(--grey-2); font-size: 1.5rem; resize: vertical; @@ -261,6 +270,7 @@ input[type=number].digit { height: 5rem; -moz-appearance: textfield; -webkit-appearance: textfield; + appearance: textfield; border-radius: 1.5rem; background-color: white; padding: 0.5rem; @@ -270,8 +280,8 @@ input[type=number].digit { input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; + -webkit-appearance: none; + margin: 0; } input[type=checkbox] { @@ -282,7 +292,7 @@ form div input[type=checkbox] { margin-right: 1rem; } -form div input:not([type=checkbox]):not([type=radio]):not([type="number"].digit):not(.btn), +form div input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type="number"].digit):not(.btn), form div textarea, form div select { margin-top: 2.5rem; @@ -338,6 +348,86 @@ form div textarea:invalid:not(:placeholder-shown)+label { color: var(--danger); } +/* Image and Logo Picker */ + +form div .picker { + display: flex; + align-items: flex-end; + height: 11rem; + gap: 1.5rem; +} + +form canvas, +form .picker .color-div { + aspect-ratio: 1/1; + height: 100%; + border: 1px solid var(--grey-2); + border-radius: 1.25rem; +} + +form .picker canvas { + background-color: var(--grey-1); +} + +form .picker .color-div { + background-color: var(--main-5); +} + +form .picker .color-selector { + display: flex; + flex-direction: column; + width: 100%; +} + +form .picker .color-input { + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; +} + +form .picker .color-input i { + font-size: 2.5rem; + padding-top: 2rem; + +} + +form .picker .color-input i:hover { + font-size: 2.5rem; + padding-top: 2rem; + color: var(--main-5); +} + + +form input[type=range].color { + appearance: none; + margin-top: 2.5rem; + box-sizing: border-box; + width: 100%; + height: 1.5rem; + border-radius: 3rem; + overflow: visible; + background: linear-gradient(to right, + hsl(0, 100%, 50%), + hsl(60, 100%, 50%), + hsl(120, 100%, 50%), + hsl(180, 100%, 50%), + hsl(240, 100%, 50%), + hsl(300, 100%, 50%), + hsl(360, 100%, 50%)); +} + +form input[type=range]::-webkit-slider-thumb, +form input[type=range]::-moz-range-thumb { + appearance: none; + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + border: 0.25rem solid var(--grey-1); + cursor: pointer; +} + /* Checkbox element */ form div.checkbox { @@ -346,11 +436,11 @@ form div.checkbox { align-items: center; } -form div.checkbox > div { - margin-right: 5rem; +form div.checkbox>div { + margin-right: 5rem; } -div.checkbox > input[type="checkbox"] { +div.checkbox>input[type="checkbox"] { display: none; width: 0; height: 0; @@ -378,7 +468,7 @@ div.checkbox:has(> input[type="checkbox"]:checked)::before { background-color: var(--color-green); } -div.checkbox > input[type="checkbox"] + label { +div.checkbox>input[type="checkbox"]+label { display: block; background-color: white; width: 2rem; @@ -393,28 +483,28 @@ div.checkbox > input[type="checkbox"] + label { transition: right 0.3s ease; } -div.checkbox:hover > input[type="checkbox"] + label { +div.checkbox:hover>input[type="checkbox"]+label { cursor: pointer; } -div.checkbox > input[type="checkbox"]:checked + label { +div.checkbox>input[type="checkbox"]:checked+label { right: 0.5rem; } /* Telephony subselect */ -div.select[data-value] ~ div.togglable { +div.select[data-value]~div.togglable { display: none; } -div.select[data-value=voicemail] ~ div.togglable.voicemail, -div.select[data-value=contact] ~ div.togglable.contact, -div.select[data-value=sip_uri] ~ div.togglable.sip_uri { +div.select[data-value=voicemail]~div.togglable.voicemail, +div.select[data-value=contact]~div.togglable.contact, +div.select[data-value=sip_uri]~div.togglable.sip_uri { display: block; } -form section.block div.checkbox:has(input:not(:checked)) ~ div { +form section.block div.checkbox:has(input:not(:checked))~div { opacity: 0.25; pointer-events: none; } \ No newline at end of file diff --git a/flexiapi/public/css/style.css b/flexiapi/public/css/style.css index c298209..5f0e6e6 100644 --- a/flexiapi/public/css/style.css +++ b/flexiapi/public/css/style.css @@ -32,7 +32,8 @@ body { --main-2: hsl(from var(--original) calc(h + 11) s calc(l + 29.80)); --main-3: hsl(from var(--original) calc(h + 8) s calc(l + 20)); --main-4: hsl(from var(--original) calc(h + 3) s calc(l + 12.35)); - --main-5: hsl(from var(--original) calc(h + 0) s calc(l + 0)); /* original */ + --main-5: hsl(from var(--original) calc(h + 0) s calc(l + 0)); + /* original */ --main-6: hsl(from var(--original) calc(h - 3) s calc(l - 7.25)); --main-7: hsl(from var(--original) calc(h - 7) s calc(l - 14.12)); @@ -285,7 +286,7 @@ header nav a#menu:after { } body.show_menu header nav a#menu:after { - font-family: 'Phosphor' + font-family: 'Phosphor'; header nav a#logo { position: absolute; @@ -300,9 +301,8 @@ header nav a#logo::before { width: 3rem; height: 3rem; padding: 1rem; - background-image: url('../img/logo.svg'); - background-color: var(--main-5); - background-size: 3rem; + background-image: var(--space-logo, url('../img/logo.svg')); + background-size: 5rem; background-position: center; background-repeat: no-repeat; display: block; diff --git a/flexiapi/public/img/logo.svg b/flexiapi/public/img/logo.svg index bf052fe..446e534 100644 --- a/flexiapi/public/img/logo.svg +++ b/flexiapi/public/img/logo.svg @@ -1,44 +1,13 @@ - - - - - - + + + + + + + + + + + + diff --git a/flexiapi/public/scripts/colorPicker.js b/flexiapi/public/scripts/colorPicker.js new file mode 100644 index 0000000..31c5f78 --- /dev/null +++ b/flexiapi/public/scripts/colorPicker.js @@ -0,0 +1,179 @@ +let colorPicker = { + init() { + let hue = document.getElementById("theme_hue").value; + document.getElementById('hex-color').value = this.hslToHex(hue, 100, 50); + document.querySelector('.color-div').style.backgroundColor = this.hslToHex(hue, 100, 50); + }, + + onSliderChange(input) { + const group = input.closest('.color-picker-group'); + const hue = input.value; + + group.querySelector('input[type=hidden]').value = hue; + group.querySelector('input[type=text]').value = this.hslToHex(hue, 100, 50); + group.querySelector('.color-div').style.backgroundColor = this.hslToHex(hue, 100, 50); + }, + + onInputChange(input) { + const hex = input.value.trim(); + if (!hex.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/)) { + return; + } + + const group = input.closest('.color-picker-group'); + const colorHsl = this.hexToHsl(hex); + group.querySelector('input[type=hidden]').value = colorHsl.h; + group.querySelector('input[type=range]').value = colorHsl.h; + group.querySelector('input[type=text]').value = this.hslToHex(colorHsl.h, 100, 50); + group.querySelector('.color-div').style.backgroundColor = this.hslToHex(colorHsl.h, 100, 50); + }, + + onReset(i, hue) { + const color = this.hslToHex(hue, 100, 50); + const group = i.closest('.color-picker-group'); + group.querySelector('input[type=hidden]').value = hue; + group.querySelector('input[type=range]').value = hue; + group.querySelector('input[type=text]').value = color; + group.querySelector('.color-div').style.backgroundColor = color; + }, + + hslToHex(h, s, l) { + s /= 100; + l /= 100; + + const k = n => (n + h / 30) % 12; + const a = s * Math.min(l, 1 - l); + const f = n => Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))))); + + const toHex = v => v.toString(16).padStart(2, '0'); + return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`; + }, + + hexToHsl(hex) { + hex = hex.replace(/^#/, ''); + if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); + + const r = parseInt(hex.slice(0, 2), 16) / 255; + const g = parseInt(hex.slice(2, 4), 16) / 255; + const b = parseInt(hex.slice(4, 6), 16) / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const d = max - min; + let h, s; + let l = (max + min) / 2; + + if (d === 0) { + h = s = 0; + } else { + s = d / (l > 0.5 ? 2 - max - min : max + min); + switch (max) { + case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; + case g: h = ((b - r) / d + 2) / 6; break; + case b: h = ((r - g) / d + 4) / 6; break; + } + } + + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + l: Math.round(l * 100), + }; + } +} + +let imagePicker = { + init() { + document.querySelectorAll('.image-picker-group').forEach(group => { + const canvas = group.querySelector('canvas'); + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + const ctx = canvas.getContext("2d"); + const existingImage = canvas.dataset.existing; + + if (existingImage) { + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => this.drawImageProp(ctx, img); + img.src = existingImage; + } else { + this.setPlaceHolder(canvas); + } + }); + }, + + onLoad(input) { + const file = input.files[0]; + if (!file) return; + if (!file.type.match('image.*')) { + console.log("Error: not an image"); + imgInput.value = ""; + return; + } + + const url = URL.createObjectURL(file); + const img = new Image(); + + const group = input.closest('.image-picker-group'); + const canvas = group.querySelector('canvas'); + const ctx = canvas.getContext("2d"); + + img.onload = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + this.drawImageProp(ctx, img); + URL.revokeObjectURL(url); + + canvas.toBlob((blob) => { + const croppedFile = new File([blob], 'logo.png', { type: 'image/png' }); + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(croppedFile); + input.files = dataTransfer.files; + }, 'image/png'); + }; + + img.src = url; + group.querySelector('input[name=logo_delete]').value = 0; + group.querySelector('input[type=button]').style.display = 'block'; + }, + + drawImageProp(ctx, img) { + const canvas = ctx.canvas; + const scale = Math.max(canvas.width / img.width, canvas.height / img.height); + + const nw = img.width * scale; + const nh = img.height * scale; + + const x = (canvas.width - nw) / 2; + const y = (canvas.height - nh) / 2; + + ctx.drawImage(img, x, y, nw, nh); + }, + + setPlaceHolder(canvas) { + const ctx = canvas.getContext("2d"); + const style = getComputedStyle(document.querySelector('body')); + ctx.fillStyle = style.getPropertyValue('--second-5').trim(); + ctx.font = `${canvas.width / 6}px ${style.getPropertyValue('font-family')}`; + ctx.textBaseline = "middle"; + + const text = "Image"; + const textWidth = ctx.measureText(text).width; + ctx.fillText(text, (canvas.width - textWidth) / 2, canvas.height / 2); + }, + + onDelete(input) { + const group = input.closest('.image-picker-group'); + const canvas = group.querySelector('canvas'); + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + this.setPlaceHolder(canvas); + group.querySelector('input[name=logo_delete]').value = 1; + group.querySelector('input[type=button]').style.display = 'none'; + } +} + +document.addEventListener("DOMContentLoaded", () => { + colorPicker.init(); + imagePicker.init(); +}); diff --git a/flexiapi/resources/views/admin/space/configuration.blade.php b/flexiapi/resources/views/admin/space/configuration.blade.php index aa538ec..6c35a21 100644 --- a/flexiapi/resources/views/admin/space/configuration.blade.php +++ b/flexiapi/resources/views/admin/space/configuration.blade.php @@ -17,12 +17,37 @@ @include('admin.space.head') @include('admin.space.tabs') -
+
@csrf @method('put') +

{{ __('Platform customization') }}

+ +
+
+ logo) data-existing="{{ asset('storage/img/' . $space->logo) }}" @endif> + + logo) style="display:none;" @endif> + + @include('parts.errors', ['name' => 'logo']) +
+
+
+ +
+
+
+
+ + + +
+ +
+
+
+
@@ -37,21 +62,24 @@
- + {{ __('An email will be sent to this email when someone join the newsletter') }} @include('parts.errors', ['name' => 'newsletter_registration_address'])
- + Will be used for informational purpose in the user panel and communication emails @include('parts.errors', ['name' => 'account_proxy_registrar_address'])
- accounts()->count() > 0)disabled @endif id="account_realm" placeholder="server.tld" value="{{ $space->account_realm }}"> + accounts()->count() > 0) disabled @endif id="account_realm" + placeholder="server.tld" value="{{ $space->account_realm }}"> A custom realm for the Space accounts @include('parts.errors', ['name' => 'account_realm']) @@ -63,31 +91,55 @@ {{ __('In ini format, will complete the other settings.') }} - {{ __('Use ; to comment, key="value" to declare a complex string.') }} {{ __('Checkout the cheatsheets to know how to format things correctly.') }} + + {{ __('Use ; to comment, key="value" to declare a complex string.') }} + {{ __('Checkout the cheatsheets to know how to format things correctly.') }} + @include('parts.errors', ['name' => 'custom_provisioning_entries'])
- @include('parts.form.toggle', ['object' => $space, 'key' => 'custom_provisioning_overwrite_all', 'label' => __('Allow client settings to be overwritten by the provisioning ones')]) + @include('parts.form.toggle', [ + 'object' => $space, + 'key' => 'custom_provisioning_overwrite_all', + 'label' => __('Allow client settings to be overwritten by the provisioning ones'), + ])
- @include('parts.form.toggle', ['object' => $space, 'key' => 'provisioning_use_linphone_provisioning_header', 'label' => 'Enforce X-Linphone-Provisioning header']) + @include('parts.form.toggle', [ + 'object' => $space, + 'key' => 'provisioning_use_linphone_provisioning_header', + 'label' => 'Enforce X-Linphone-Provisioning header', + ])

{{ __('Features') }}

- @include('parts.form.toggle', ['object' => $space, 'key' => 'public_registration', 'label' => __('Public registration')]) -
-
$space, 'key' => 'phone_registration', 'label' => __('Phone registration')]) + @include('parts.form.toggle', [ + 'object' => $space, + 'key' => 'public_registration', + 'label' => __('Public registration'), + ])
- @include('parts.form.toggle', ['object' => $space, 'key' => 'intercom_features', 'label' => __('Intercom features')]) + @include('parts.form.toggle', [ + 'object' => $space, + 'key' => 'phone_registration', + 'label' => __('Phone registration'), + ]) +
+
+ @include('parts.form.toggle', [ + 'object' => $space, + 'key' => 'intercom_features', + 'label' => __('Intercom features'), + ])
+ @endsection diff --git a/flexiapi/resources/views/api/documentation/spaces.blade.php b/flexiapi/resources/views/api/documentation/spaces.blade.php index 270aae1..ae84223 100644 --- a/flexiapi/resources/views/api/documentation/spaces.blade.php +++ b/flexiapi/resources/views/api/documentation/spaces.blade.php @@ -51,6 +51,7 @@ JSON parameters: * `public_registration` boolean, the public registration switch * `super` boolean, set the domain as a Super Domain * `web_panel` boolean, the web panel switch +* `theme_hue` integer, the hue component of an HSL color (e.g. `hsl(theme_hue, 100%, 50%)`), between 0 and 360 ### `PUT /spaces/{domain}` Super Admin @@ -88,6 +89,7 @@ JSON parameters: * `public_registration` **required**, boolean, the public registration switch * `super` **required**, boolean, set the domain as a Super Domain * `web_panel` **required**, boolean, the web panel switch +* `theme_hue` **required**, integer, the hue component of an HSL color (e.g. `hsl(theme_hue, 100%, 50%)`), between 0 and 360 ### `DELETE /spaces/{domain}` Super Admin diff --git a/flexiapi/resources/views/layouts/main.blade.php b/flexiapi/resources/views/layouts/main.blade.php index c820fd3..6a0d6e4 100644 --- a/flexiapi/resources/views/layouts/main.blade.php +++ b/flexiapi/resources/views/layouts/main.blade.php @@ -9,15 +9,15 @@ {{ space()->name }} - - @php - $space = space(); - @endphp - + @if (space()?->custom_theme && file_exists(public_path('css/' . space()?->host . '.style.css'))) @endif - @@ -26,8 +26,9 @@