From 5ef021c3812794a9d9a7461681e531ab350e2bc0 Mon Sep 17 00:00:00 2001
From: Deon George <deon@dege.au>
Date: Sun, 23 Mar 2025 22:16:26 +1100
Subject: [PATCH] Display a DN entry with language tags - work for #16

---
 app/Classes/LDAP/Attribute.php                | 47 +++++++++++---
 app/Ldap/Entry.php                            |  4 +-
 app/View/Components/Attribute.php             |  6 +-
 app/View/Components/AttributeType.php         |  7 +-
 public/css/custom.css                         |  3 +
 .../views/components/attribute-type.blade.php |  2 +-
 .../views/components/attribute.blade.php      | 37 ++++-------
 .../attribute/objectclass.blade.php           |  4 +-
 .../components/attribute/password.blade.php   |  2 +-
 .../attribute/widget/options.blade.php        |  2 +-
 resources/views/fragment/dn/header.blade.php  |  4 +-
 resources/views/frames/dn.blade.php           | 64 +++++++++++++++++--
 12 files changed, 132 insertions(+), 50 deletions(-)

diff --git a/app/Classes/LDAP/Attribute.php b/app/Classes/LDAP/Attribute.php
index af398bc1..83abe2e8 100644
--- a/app/Classes/LDAP/Attribute.php
+++ b/app/Classes/LDAP/Attribute.php
@@ -178,14 +178,7 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
 		return $this->name;
 	}
 
-	public function addValue(string $tag,string $value): void
-	{
-		$this->_values->put(
-			$tag,
-			$this->_values
-				->get($tag,collect())
-				->push($value));
-	}
+	/* INTERFACE */
 
 	public function current(): mixed
 	{
@@ -237,6 +230,24 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
 		// We cannot clear values using array syntax
 	}
 
+	/* METHODS */
+
+	public function addValue(string $tag,array $values): void
+	{
+		$this->_values->put(
+			$tag,
+			array_unique(array_merge($this->_values
+				->get($tag,[]),$values)));
+	}
+
+	public function addValueOld(string $tag,array $values): void
+	{
+		$this->_values_old->put(
+			$tag,
+			array_unique(array_merge($this->_values_old
+				->get($tag,[]),$values)));
+	}
+
 	/**
 	 * Return the hints about this attribute, ie: RDN, Required, etc
 	 *
@@ -266,8 +277,10 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
 	 */
 	public function isDirty(): bool
 	{
-		return ($this->values_old->count() !== $this->values->count())
-			|| ($this->values->diff($this->values_old)->count() !== 0);
+		return (($a=$this->values_old->dot())->keys()->count() !== ($b=$this->values->dot())->keys()->count())
+			|| ($a->values()->count() !== $b->values()->count())
+			|| ($a->keys()->diff($b->keys())->count() !== 0)
+			|| ($a->values()->diff($b->values())->count() !== 0);
 	}
 
 	/**
@@ -329,4 +342,18 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
 			? $this->oc->intersect($this->required_by->keys())->sort()
 			: collect();
 	}
+
+	public function tagValues(string $tag=''): Collection
+	{
+		return $this->_values
+			->filter(fn($item,$key)=>($key === $tag))
+			->values();
+	}
+
+	public function tagValuesOld(string $tag=''): Collection
+	{
+		return $this->_values_old
+			->filter(fn($item,$key)=>($key === $tag))
+			->values();
+	}
 }
\ No newline at end of file
diff --git a/app/Ldap/Entry.php b/app/Ldap/Entry.php
index 6754f38a..73ce9d37 100644
--- a/app/Ldap/Entry.php
+++ b/app/Ldap/Entry.php
@@ -309,7 +309,9 @@ class Entry extends Model
 			->map(fn($item)=>$item
 				->values
 				->keys()
-				->filter(fn($item)=>preg_match(sprintf('/%s+;?/',self::TAG_CHARS_LANG),$item)))
+				->filter(fn($item)=>preg_match(sprintf('/%s+;?/',self::TAG_CHARS_LANG),$item))
+				->map(fn($item)=>preg_replace('/lang-/','',$item))
+			)
 			->filter(fn($item)=>$item->count());
 	}
 
diff --git a/app/View/Components/Attribute.php b/app/View/Components/Attribute.php
index fd866f93..0e78ee4c 100644
--- a/app/View/Components/Attribute.php
+++ b/app/View/Components/Attribute.php
@@ -12,17 +12,19 @@ class Attribute extends Component
 	public bool $edit;
 	public bool $new;
 	public bool $old;
-	public ?string $na;
+	public string $langtag;
+	public ?string $na;	// Text to render if the LDAPAttribute is null
 
     /**
      * Create a new component instance.
      */
-    public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,?string $na=NULL)
+    public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,string $langtag='',?string $na=NULL)
     {
 		$this->o = $o;
 		$this->edit = $edit;
 		$this->old = $old;
 		$this->new = $new;
+		$this->langtag = $langtag;
 		$this->na = $na;
     }
 
diff --git a/app/View/Components/AttributeType.php b/app/View/Components/AttributeType.php
index 6c3dcb57..b7af118c 100644
--- a/app/View/Components/AttributeType.php
+++ b/app/View/Components/AttributeType.php
@@ -13,14 +13,16 @@ class AttributeType extends Component
 {
 	private LDAPAttribute $o;
 	private bool $new;
+	private string $langtag;
 
 	/**
 	 * Create a new component instance.
 	 */
-	public function __construct(LDAPAttribute $o,bool $new=FALSE)
+	public function __construct(LDAPAttribute $o,bool $new=FALSE,string $langtag='')
 	{
 		$this->o = $o;
 		$this->new = $new;
+		$this->langtag = $langtag;
 	}
 
 	/**
@@ -30,6 +32,7 @@ class AttributeType extends Component
 	{
 		return view('components.attribute-type')
 			->with('o',$this->o)
-			->with('new',$this->new);
+			->with('new',$this->new)
+			->with('langtag',$this->langtag);
 	}
 }
\ No newline at end of file
diff --git a/public/css/custom.css b/public/css/custom.css
index a0b522fe..d61392fa 100644
--- a/public/css/custom.css
+++ b/public/css/custom.css
@@ -30,7 +30,10 @@ input.form-control.input-group-end {
 
 .custom-tooltip-danger {
 	--bs-tooltip-bg: var(--bs-danger);
+}
 
+.custom-tooltip {
+	--bs-tooltip-bg: var(--bs-gray-900);
 }
 
 .tooltip {
diff --git a/resources/views/components/attribute-type.blade.php b/resources/views/components/attribute-type.blade.php
index 9fa95a38..feb96553 100644
--- a/resources/views/components/attribute-type.blade.php
+++ b/resources/views/components/attribute-type.blade.php
@@ -15,7 +15,7 @@
 			</div>
 		</div>
 
-		<x-attribute :o="$o" :edit="true" :new="$new ?? FALSE"/>
+		<x-attribute :o="$o" :edit="true" :new="$new ?? FALSE" :langtag="$langtag"/>
 	</div>
 </div>
 
diff --git a/resources/views/components/attribute.blade.php b/resources/views/components/attribute.blade.php
index db84c49c..ace574a3 100644
--- a/resources/views/components/attribute.blade.php
+++ b/resources/views/components/attribute.blade.php
@@ -1,31 +1,22 @@
 <!-- $o=Attribute::class -->
 <x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
-	@foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->values) as $tag=>$tagvalues)
-		<div class="row p-2 border rounded">
-			<div class="col-2">
-				{{ $tag }}
-			</div>
-			<div class="col-10">
-				<div class="row">
-					<div class="col-12">
-						@foreach($tagvalues as $value)
-							@if(($edit ?? FALSE) && ! $o->is_rdn)
-								<div class="input-group has-validation">
-									<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>($o->values->contains($value))]) name="{{ $o->name_lc }}[]" value="{{ $value }}" placeholder="{{ ! is_null($x=Arr::get($o->values,$loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(! ($new ?? FALSE))>
+	@foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->tagValues($langtag)) as $values)
+		<div class="col-12">
+			@foreach($values as $value)
+				@if(($edit ?? FALSE) && ! $o->is_rdn)
+					<div class="input-group has-validation">
+						<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>($o->values->contains($value))]) name="{{ $o->name_lc }}[]" value="{{ $value }}" placeholder="{{ ! is_null($x=Arr::get($o->values,$loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(! ($new ?? FALSE))>
 
-									<div class="invalid-feedback pb-2">
-										@if($e)
-											{{ join('|',$e) }}
-										@endif
-									</div>
-								</div>
-							@else
-								{{ $value }}
+						<div class="invalid-feedback pb-2">
+							@if($e)
+								{{ join('|',$e) }}
 							@endif
-						@endforeach
+						</div>
 					</div>
-				</div>
-			</div>
+				@else
+					{{ $value }}
+				@endif
+			@endforeach
 		</div>
 	@endforeach
 </x-attribute.layout>
\ No newline at end of file
diff --git a/resources/views/components/attribute/objectclass.blade.php b/resources/views/components/attribute/objectclass.blade.php
index 22b9f7f4..02522d9d 100644
--- a/resources/views/components/attribute/objectclass.blade.php
+++ b/resources/views/components/attribute/objectclass.blade.php
@@ -1,8 +1,8 @@
 <!-- $o=Attribute::class -->
-<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
+<x-attribute.layout :edit="$edit" :new="$new" :o="$o" :langtag="$langtag">
 	@foreach(old($o->name_lc,$o->values) as $value)
 		@if($edit)
-			<x-attribute.widget.objectclass :o="$o" :edit="$edit" :new="$new" :loop="$loop" :value="$value"/>
+			<x-attribute.widget.objectclass :o="$o" :edit="$edit" :new="$new" :loop="$loop" :value="$value" :langtag="$langtag"/>
 		@else
 			{{ $value }}
 			@if ($o->isStructural($value))
diff --git a/resources/views/components/attribute/password.blade.php b/resources/views/components/attribute/password.blade.php
index 943f4b6b..8448ebea 100644
--- a/resources/views/components/attribute/password.blade.php
+++ b/resources/views/components/attribute/password.blade.php
@@ -1,6 +1,6 @@
 <!-- @todo We are not handling redirect backs yet with updated passwords -->
 <!-- $o=Password::class -->
-<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
+<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o" :langtag="$langtag">
 	@foreach($o->values_old as $value)
 		@if($edit)
 			<div class="input-group has-validation mb-3">
diff --git a/resources/views/components/attribute/widget/options.blade.php b/resources/views/components/attribute/widget/options.blade.php
index eaf55b48..37097185 100644
--- a/resources/views/components/attribute/widget/options.blade.php
+++ b/resources/views/components/attribute/widget/options.blade.php
@@ -4,7 +4,7 @@
 <span class="p-0 m-0">
 	@if($o->is_rdn)
 		<br/>
-		<span class="btn btn-sm btn-outline-focus mt-3" disabled><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</span>
+		<button class="btn btn-sm btn-outline-focus mt-3" disabled><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</button>
 	@elseif($edit && $o->can_addvalues)
 		@switch(get_class($o))
 			@case(JpegPhoto::class)
diff --git a/resources/views/fragment/dn/header.blade.php b/resources/views/fragment/dn/header.blade.php
index fddc54d1..0160c9f3 100644
--- a/resources/views/fragment/dn/header.blade.php
+++ b/resources/views/fragment/dn/header.blade.php
@@ -26,10 +26,10 @@
 						<x-attribute :o="$o->getObject('entryuuid')" :na="__('Unknown')"/>
 					</th>
 				</tr>
-				@if(($x=$o->getLangTags())->count())
+				@if($langtags->count())
 					<tr class="mt-1">
 						<td class="p-0 pe-2">Tags</td>
-						<th class="p-0">{{ $x->flatMap(fn($item)=>$item->values())->unique()->join(', ') }}</th>
+						<th class="p-0">{{ $langtags->join(', ') }}</th>
 					</tr>
 				@endif
 			</table>
diff --git a/resources/views/frames/dn.blade.php b/resources/views/frames/dn.blade.php
index 931352ef..60081b80 100644
--- a/resources/views/frames/dn.blade.php
+++ b/resources/views/frames/dn.blade.php
@@ -1,7 +1,13 @@
 @extends('layouts.dn')
 
 @section('page_title')
-	@include('fragment.dn.header',['o'=>($o ?? $o=$server->fetch($dn))])
+	@include('fragment.dn.header',[
+		'o'=>($o ?? $o=$server->fetch($dn)),
+		'langtags'=>($langtags=$o->getLangTags()
+			->flatMap(fn($item)=>$item->values())
+			->unique()
+			->sort())
+	])
 @endsection
 
 @section('page_actions')
@@ -71,7 +77,7 @@
 	<div class="main-card mb-3 card">
 		<div class="card-body">
 			<div class="card-header-tabs">
-				<ul class="nav nav-tabs">
+				<ul class="nav nav-tabs mb-0">
 					<li class="nav-item"><a data-bs-toggle="tab" href="#attributes" class="nav-link active">@lang('Attributes')</a></li>
 					<li class="nav-item"><a data-bs-toggle="tab" href="#internal" class="nav-link">@lang('Internal')</a></li>
 					@env(['local'])
@@ -87,9 +93,57 @@
 
 							<input type="hidden" name="dn" value="">
 
-							@foreach ($o->getVisibleAttributes() as $ao)
-								<x-attribute-type :edit="true" :o="$ao"/>
-							@endforeach
+							<div class="card-header border-bottom-0">
+								<div class="btn-actions-pane-right">
+									<div role="group" class="btn-group-sm nav btn-group">
+										@foreach($langtags->prepend('')->push('+') as $tag)
+											<a data-bs-toggle="tab" href="#tab-lang-{{ $tag ?: '_default' }}" class="btn btn-outline-light border-dark-subtle @if(! $loop->index) active @endif @if($loop->last)ndisabled @endif">
+												@switch($tag)
+													@case('')
+														<i class="fas fa-fw fa-border-none" data-bs-toggle="tooltip" data-bs-custom-class="custom-tooltip" title="@lang('No Lang Tag')"></i>
+														@break
+
+													@case('+')
+														<!-- @todo To implement -->
+														<i class="fas fa-fw fa-plus text-dark" data-bs-toggle="tooltip" data-bs-custom-class="custom-tooltip" title="@lang('Add Lang Tag')"></i>
+														@break
+
+													@default
+														<span class="f16" data-bs-toggle="tooltip" data-bs-custom-class="custom-tooltip" title="{{ strtoupper($tag) }}"><i class="flag {{ $tag }}"></i></span>
+												@endswitch
+											</a>
+										@endforeach
+									</div>
+								</div>
+							</div>
+
+							<div class="card-body">
+								<div class="tab-content">
+									@foreach($langtags as $tag)
+										<div class="tab-pane @if(! $loop->index) active @endif" id="tab-lang-{{ $tag ?: '_default' }}" role="tabpanel">
+											@switch($tag)
+												@case('')
+													@foreach ($o->getVisibleAttributes($tag) as $ao)
+														<x-attribute-type :edit="true" :o="$ao" langtag=""/>
+													@endforeach
+													@break
+
+												@case('+')
+													<div class="ms-auto mt-4 alert alert-warning p-2" style="max-width: 30em; font-size: 0.80em;">
+														It is not possible to create new language tags at the moment. This functionality should come soon.<br>
+														You can create them with an LDIF import though.
+													</div>
+													@break
+
+												@default
+													@foreach ($o->getVisibleAttributes($langtag=sprintf('lang-%s',$tag)) as $ao)
+														<x-attribute-type :edit="true" :o="$ao" :langtag="$langtag"/>
+													@endforeach
+											@endswitch
+										</div>
+									@endforeach
+								</div>
+							</div>
 
 							@include('fragment.dn.add_attr')
 						</form>