Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
'field_conditions_instructions' => 'When to show or hide this field.',
'field_desynced_from_origin' => 'Desynced from origin. Click to sync and revert to the origin\'s value.',
'field_synced_with_origin' => 'Synced with origin. Click or edit the field to desync.',
'localization_fully_synced_with_origin' => 'All fields synced with origin.',
'field_validation_advanced_instructions' => 'Add more advanced validation to this field.',
'field_validation_required_instructions' => 'Make this field required or optional.',
'field_validation_sometimes_instructions' => 'Only validate when this field is visible or submitted.',
Expand Down
27 changes: 26 additions & 1 deletion resources/js/components/SiteSelector.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup>
import { computed } from 'vue';
import { Icon, Select, Subheading } from '@/components/ui';
import { Badge, Icon, Select, Subheading } from '@/components/ui';
import {
flatOptionsFromSiteGroups,
groupItemsBySiteGroup,
Expand Down Expand Up @@ -49,6 +49,31 @@ function groupLabel(option) {
<Icon name="chevron-right" class="size-3.5! shrink-0 text-gray-700 dark:text-white/70" aria-hidden="true" />
</template>
<span class="truncate">{{ __(option.name) }}</span>
<span
v-if="option.fully_synced === true"
class="inline-flex shrink-0 ps-0.5"
v-tooltip="__('messages.localization_fully_synced_with_origin')"
>
<Icon name="synced" class="size-3.5! text-gray-400 dark:text-gray-600" />
</span>
</span>
</template>

<template #option="option">
<span class="flex min-w-0 flex-1 items-center gap-x-2">
<span class="flex min-w-0 items-center gap-1.5">
<span class="truncate">{{ __(option.name) }}</span>
<span
v-if="option.fully_synced === true"
class="inline-flex shrink-0"
v-tooltip="__('messages.localization_fully_synced_with_origin')"
>
<Icon name="synced" class="size-3.5! text-gray-400 dark:text-gray-600" />
</span>
</span>
<Badge size="sm" color="orange" v-if="option.origin === true" :text="__('Origin')" />
<Badge size="sm" color="blue" v-if="option.active === true" :text="__('Active')" />
<Badge size="sm" color="purple" v-if="option.root === true && option.origin !== true && option.active !== true" :text="__('Root')" />
</span>
</template>

Expand Down
25 changes: 25 additions & 0 deletions resources/js/components/globals/PublishForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,33 @@ export default {
saving(saving) {
this.$progress.loading(`${this.publishContainer}-global-publish-form`, saving);
},

localizedFields: {
handler() {
this.refreshActiveLocalizationSyncState();
},
deep: true,
},

hasOrigin() {
this.refreshActiveLocalizationSyncState();
},
},

methods: {
refreshActiveLocalizationSyncState() {
this.localizations = this.localizations.map((localization) => {
if (localization.handle !== this.site) {
return localization;
}

return {
...localization,
fully_synced: this.hasOrigin && this.localizedFields.length === 0,
};
});
},

save() {
if (!this.canSave) return;

Expand All @@ -262,6 +286,7 @@ export default {
.then((response) => {
if (!this.isCreating) this.$toast.success(__('Saved'));

this.refreshActiveLocalizationSyncState();
this.$nextTick(() => this.$emit('saved', response));
})
.catch((e) => {
Expand Down
15 changes: 10 additions & 5 deletions src/Http/Controllers/CP/Globals/GlobalVariablesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ public function edit(Request $request, $id)
'group' => $site->group(),
'group_handle' => $site->groupHandle(),
'active' => $localized->locale() === $variables->locale(),
'origin' => ! $localized->hasOrigin(),
'root' => $localized->isRoot(),
'origin' => optional($variables->origin())->locale() === $localized->locale(),
'fully_synced' => $localized->hasOrigin() && $localized->data()->isEmpty(),
'url' => $localized->editUrl(),
];
})->values()->all(),
Expand Down Expand Up @@ -137,9 +139,12 @@ protected function extractFromFields($set, $blueprint)

protected function getAuthorizedLocalizationsForVariables($variables)
{
return $variables
->globalSet()
->localizations()
->filter(fn ($set) => User::current()->can('edit', $set));
$localizations = $variables->globalSet()->localizations();

return Site::all()
->map(fn ($site) => $localizations->get($site->handle()))
->filter()
->filter(fn ($set) => User::current()->can('edit', $set))
->values();
}
}
179 changes: 179 additions & 0 deletions tests/Feature/Globals/EditGlobalVariablesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,185 @@ public function it_shows_the_form_even_if_localization_does_not_exist()
);
}

#[Test]
public function it_marks_localizations_as_fully_synced_when_they_have_no_localized_data()
{
$this->setSites([
'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'],
'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'],
]);

$blueprint = Blueprint::make()->setContents(['fields' => [
['handle' => 'foo', 'field' => ['type' => 'text']],
]]);
Blueprint::partialMock();
Blueprint::shouldReceive('find')->with('globals.test')->andReturn($blueprint);

$this->setTestRoles(['test' => [
'access cp',
'edit test globals',
'access en site',
'access fr site',
]]);
$user = User::make()->assignRole('test')->save();

$global = GlobalSet::make('test')->sites(['en', 'fr' => 'en'])->save();
$global->in('en')->data(['foo' => 'bar'])->save();
$global->in('fr')->data(['foo' => 'baz'])->save();

$this
->actingAs($user)
->get($global->in('en')->editUrl())
->assertSuccessful()
->assertInertia(fn (Assert $page) => $page
->component('globals/Edit')
->where('localizations.0.handle', 'en')
->where('localizations.0.active', true)
->where('localizations.0.root', true)
->where('localizations.0.origin', false)
->where('localizations.0.fully_synced', false)
->where('localizations.1.handle', 'fr')
->where('localizations.1.active', false)
->where('localizations.1.root', false)
->where('localizations.1.origin', false)
->where('localizations.1.fully_synced', false)
);

$this
->actingAs($user)
->get($global->in('fr')->editUrl())
->assertSuccessful()
->assertInertia(fn (Assert $page) => $page
->component('globals/Edit')
->where('localizations.0.handle', 'en')
->where('localizations.0.active', false)
->where('localizations.0.root', true)
->where('localizations.0.origin', true)
->where('localizations.1.handle', 'fr')
->where('localizations.1.active', true)
->where('localizations.1.root', false)
->where('localizations.1.origin', false)
->where('localizations.1.fully_synced', false)
);

$global->in('fr')->data([])->save();

$this
->actingAs($user)
->get($global->in('en')->editUrl())
->assertSuccessful()
->assertInertia(fn (Assert $page) => $page
->component('globals/Edit')
->where('localizations.0.handle', 'en')
->where('localizations.0.fully_synced', false)
->where('localizations.1.handle', 'fr')
->where('localizations.1.fully_synced', true)
);

// Matching origin values still counts as unsynced once data is stored locally.
$global->in('fr')->data(['foo' => 'bar'])->save();

$this
->actingAs($user)
->get($global->in('en')->editUrl())
->assertSuccessful()
->assertInertia(fn (Assert $page) => $page
->component('globals/Edit')
->where('localizations.1.handle', 'fr')
->where('localizations.1.fully_synced', false)
);
}

#[Test]
public function it_marks_root_when_the_origin_is_not_the_root()
{
$this->setSites([
'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'],
'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'],
'de' => ['name' => 'German', 'locale' => 'de_DE', 'url' => 'http://de.test.com/'],
]);

$blueprint = Blueprint::make()->setContents(['fields' => [
['handle' => 'foo', 'field' => ['type' => 'text']],
]]);
Blueprint::partialMock();
Blueprint::shouldReceive('find')->with('globals.test')->andReturn($blueprint);

$this->setTestRoles(['test' => [
'access cp',
'edit test globals',
'access en site',
'access fr site',
'access de site',
]]);
$user = User::make()->assignRole('test')->save();

$global = GlobalSet::make('test')->sites(['en', 'fr' => 'en', 'de' => 'fr'])->save();
$global->in('en')->data(['foo' => 'bar'])->save();
$global->in('fr')->data([])->save();
$global->in('de')->data([])->save();

$this
->actingAs($user)
->get($global->in('de')->editUrl())
->assertSuccessful()
->assertInertia(fn (Assert $page) => $page
->component('globals/Edit')
->where('localizations.0.handle', 'en')
->where('localizations.0.root', true)
->where('localizations.0.origin', false)
->where('localizations.0.active', false)
->where('localizations.1.handle', 'fr')
->where('localizations.1.root', false)
->where('localizations.1.origin', true)
->where('localizations.1.active', false)
->where('localizations.2.handle', 'de')
->where('localizations.2.root', false)
->where('localizations.2.origin', false)
->where('localizations.2.active', true)
);
}

#[Test]
public function it_orders_localizations_by_configured_site_order()
{
$this->setSites([
'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/', 'group' => 'EU', 'group_handle' => 'eu'],
'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/', 'group' => 'UK', 'group_handle' => 'uk'],
]);

$blueprint = Blueprint::make()->setContents(['fields' => [
['handle' => 'foo', 'field' => ['type' => 'text']],
]]);
Blueprint::partialMock();
Blueprint::shouldReceive('find')->with('globals.test')->andReturn($blueprint);

$this->setTestRoles(['test' => [
'access cp',
'edit test globals',
'access en site',
'access fr site',
]]);
$user = User::make()->assignRole('test')->save();

// Global set sites() keys start with en, but Site::all() starts with fr.
$global = GlobalSet::make('test')->sites(['en', 'fr' => 'en'])->save();
$global->in('en')->data(['foo' => 'bar'])->save();
$global->in('fr')->data([])->save();

$this
->actingAs($user)
->get($global->in('en')->editUrl())
->assertSuccessful()
->assertInertia(fn (Assert $page) => $page
->component('globals/Edit')
->where('localizations.0.handle', 'fr')
->where('localizations.0.group', 'EU')
->where('localizations.1.handle', 'en')
->where('localizations.1.group', 'UK')
);
}

#[Test]
public function it_404s_if_invalid_site()
{
Expand Down
Loading