1010use Illuminate \Database \Eloquent \Model ;
1111use Illuminate \Support \Facades \Auth ;
1212use Illuminate \Support \Facades \Schema ;
13+ use Illuminate \Support \Str ;
1314use Moox \Core \Models \Scope ;
1415use Moox \Core \Services \ScopeAssignmentValidator ;
1516use Moox \Core \Services \ScopeRegistry ;
@@ -20,6 +21,124 @@ trait HasScopedChildResource
2021{
2122 public const ASSIGN_GLOBAL_SCOPE = '__global__ ' ;
2223
24+ public static function canAssignScopes (): bool
25+ {
26+ return static ::hasMultipleAssignableScopes ();
27+ }
28+
29+ public static function formatScopeForDisplay (?string $ scope ): string
30+ {
31+ if ($ scope === null || $ scope === '' ) {
32+ return 'Global ' ;
33+ }
34+
35+ $ parsed = ScopeValue::parse ($ scope );
36+ if ($ parsed === null ) {
37+ return $ scope ;
38+ }
39+
40+ $ origin = static ::humanizeScopeSegment ($ parsed ->origin ());
41+ $ source = static ::humanizeScopeSegment ($ parsed ->source ());
42+ $ context = static ::humanizeScopeSegment ($ parsed ->context ());
43+ $ boundary = static ::humanizeScopeSegment ($ parsed ->boundary ());
44+
45+ return "{$ origin } → {$ source } → {$ context } ( {$ boundary }) " ;
46+ }
47+
48+ protected static function humanizeScopeSegment (string $ value ): string
49+ {
50+ return Str::headline (Str::snake ($ value ));
51+ }
52+
53+ /**
54+ * @return array<string, string>
55+ */
56+ public static function getAssignableScopeOptionsForRecord (?Model $ record = null ): array
57+ {
58+ return static ::getAssignableScopeOptions ();
59+ }
60+
61+ public static function getDefaultAssignableScopeForRecord (?Model $ record = null ): string
62+ {
63+ if ($ record && is_string ($ record ->scope ) && $ record ->scope !== '' ) {
64+ $ options = static ::getAssignableScopeOptions ();
65+ if (array_key_exists ($ record ->scope , $ options )) {
66+ return $ record ->scope ;
67+ }
68+ }
69+
70+ if ($ record && ($ record ->scope === null || $ record ->scope === '' )) {
71+ return static ::ASSIGN_GLOBAL_SCOPE ;
72+ }
73+
74+ return static ::getDefaultAssignableScope ();
75+ }
76+
77+ /**
78+ * @return array{updated: bool, message?: string}
79+ */
80+ public static function assignScopeToRecord (Model $ record , string $ selectedScope ): array
81+ {
82+ if (! static ::recordSupportsScopeColumn ($ record )) {
83+ return ['updated ' => false , 'message ' => 'This record is not scopable. ' ];
84+ }
85+
86+ $ actor = Auth::user ();
87+ $ validator = app (ScopeAssignmentValidator::class);
88+
89+ $ targetScope = $ selectedScope === static ::ASSIGN_GLOBAL_SCOPE ? '' : $ selectedScope ;
90+
91+ $ validation = $ validator ->validate ($ record , $ targetScope , $ actor );
92+ if (! ($ validation ['allowed ' ] ?? false )) {
93+ return ['updated ' => false , 'message ' => 'Target scope was inactive or boundary rules were not fulfilled. ' ];
94+ }
95+
96+ if ($ selectedScope === static ::ASSIGN_GLOBAL_SCOPE ) {
97+ $ record ->setAttribute ('scope ' , null );
98+ } else {
99+ $ record ->setAttribute ('scope ' , ScopeValue::toStringOrNull ($ selectedScope ));
100+ }
101+
102+ $ record ->save ();
103+
104+ return ['updated ' => true ];
105+ }
106+
107+ public static function getScopeSelectField (string $ name = 'scope ' ): Select
108+ {
109+ return Select::make ($ name )
110+ ->label ('Scope ' )
111+ ->dehydrated (false )
112+ ->live ()
113+ ->options (fn (?Model $ record ) => static ::getAssignableScopeOptionsForRecord ($ record ))
114+ ->default (fn (?Model $ record ) => static ::getDefaultAssignableScopeForRecord ($ record ))
115+ ->afterStateUpdated (function ($ state , ?Model $ record , Select $ component ): void {
116+ if (! $ record ) {
117+ return ;
118+ }
119+
120+ $ result = static ::assignScopeToRecord ($ record , (string ) $ state );
121+
122+ if (! ($ result ['updated ' ] ?? false )) {
123+ Notification::make ()
124+ ->warning ()
125+ ->title ('Scope not allowed ' )
126+ ->body ($ result ['message ' ] ?? 'Unable to update scope. ' )
127+ ->send ();
128+
129+ $ record ->refresh ();
130+ $ component ->state (static ::getDefaultAssignableScopeForRecord ($ record ));
131+
132+ return ;
133+ }
134+
135+ Notification::make ()
136+ ->success ()
137+ ->title ('Scope updated ' )
138+ ->send ();
139+ });
140+ }
141+
23142 public static function scopeQuery (Builder $ query ): Builder
24143 {
25144 return ScopedResourceContext::applyScope ($ query , static ::class);
@@ -170,8 +289,19 @@ protected static function getAssignableScopeOptions(): array
170289 ->toArray ();
171290
172291 foreach ($ rows as $ scope => $ row ) {
173- $ base = $ row ['label ' ] ?: $ row ['scope ' ];
174- $ options [$ scope ] = "{$ base } — {$ row ['source ' ]}/ {$ row ['context ' ]} ( {$ row ['boundary ' ]}) " ;
292+ $ parsed = ScopeValue::parse ($ scope );
293+ if ($ parsed === null ) {
294+ $ options [$ scope ] = $ scope ;
295+
296+ continue ;
297+ }
298+
299+ $ originLabel = static ::humanizeScopeSegment ($ parsed ->origin ());
300+ $ sourceLabel = static ::humanizeScopeSegment ($ parsed ->source ());
301+ $ contextLabel = static ::humanizeScopeSegment ($ parsed ->context ());
302+ $ boundaryLabel = static ::humanizeScopeSegment ($ parsed ->boundary ());
303+
304+ $ options [$ scope ] = "{$ originLabel } → {$ sourceLabel } → {$ contextLabel } ( {$ boundaryLabel }) " ;
175305 }
176306
177307 return $ options ;
0 commit comments