Skip to content
Merged
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
39 changes: 32 additions & 7 deletions resources/js/components/field-conditions/Builder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const props = defineProps({
config: { type: Object, required: true },
suggestableFields: { type: Array, required: true },
allowCustomConditions: { type: Boolean, default: true },
showAlwaysHideOption: { type: Boolean, default: false },
showAlwaysSave: { type: Boolean, default: true },
size: { type: String, default: 'base' },
});
Expand All @@ -23,11 +24,14 @@ const customMethod = ref(null);
const conditions = ref([]);
const alwaysSave = ref(false);

const whenOptions = [
{ label: __('Always show'), value: 'always', icon: 'eye' },
{ label: __('Show when'), value: 'if', icon: 'eye' },
{ label: __('Hide when'), value: 'unless', icon: 'eye-closed' },
];
const whenOptions = computed(() => {
return [
{ label: __('Always show'), value: 'always', icon: 'eye' },
props.showAlwaysHideOption ? { label: __('Always hide'), value: 'always_hide', icon: 'eye-closed' } : null,
{ label: __('Show when'), value: 'if', icon: 'eye' },
{ label: __('Hide when'), value: 'unless', icon: 'eye-closed' },
].filter(Boolean);
});

const joinOptions = [
{ label: __('All of the conditions pass'), short_label: __('And'), value: 'all' },
Expand All @@ -36,7 +40,7 @@ const joinOptions = [
];

const isCustom = computed(() => type.value === 'custom');
const hasConditions = computed(() => when.value !== 'always');
const hasConditions = computed(() => when.value !== 'always' && when.value !== 'always_hide');

const add = () => {
conditions.value.push({
Expand All @@ -52,7 +56,9 @@ const remove = (index) => conditions.value.splice(index, 1);
const toggleCustom = () => type.value = isCustom.value ? 'all' : 'custom';

const saveableConditions = computed(() => {
const result = {};
const result = Object.fromEntries(KEYS.map((key) => [key, null]));
if (!hasConditions.value) return result;

const key = type.value === 'any' ? `${when.value}_any` : when.value;
const filtered = conditions.value.filter((c) => c.field && c.value);
const prepared = new Converter().toBlueprint(filtered);
Expand All @@ -78,6 +84,12 @@ const prepareEditableOperator = (operator) => {
return operator;
};

const getInitialWhenState = () => {
if (props.showAlwaysHideOption) {
when.value = props.config.hidden ? 'always_hide' : 'always';
}
};

const getInitialConditions = () => {
const key = KEYS.find((k) => props.config[k]);
const configConditions = key ? props.config[key] : null;
Expand All @@ -102,6 +114,18 @@ const getInitialConditions = () => {

const getInitialAlwaysSaveState = () => alwaysSave.value = props.config?.always_save ?? false;

watch(() => props.config.hidden, (hidden) => {
if (hidden) {
when.value = 'always_hide';
} else if (when.value === 'always_hide') {
when.value = 'always';
}
});

watch(when, (value) => {
if (initialized.value) emit('updated', { hidden: value === 'always_hide' });
});

watch(saveableConditions, (conditions) => {
if (initialized.value) emit('updated', conditions);
}, { deep: true });
Expand All @@ -111,6 +135,7 @@ watch(alwaysSave, (value) => {
});

onMounted(() => {
getInitialWhenState();
getInitialConditions();
getInitialAlwaysSaveState();
if (conditions.value.length === 0) add();
Expand Down
7 changes: 4 additions & 3 deletions resources/js/components/forms/builder/FieldInspector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const errors = computed(() => {

enum FieldInspectorTabs {
Settings = 'settings',
Conditions = 'conditions',
Logic = 'logic',
Validation = 'validation',
}

Expand Down Expand Up @@ -205,7 +205,7 @@ onMounted(() => load());
<Tabs v-else v-model:modelValue="activeTab" :unmount-on-hide="false">
<TabList class="inline-flex flex-wrap [&_button]:w-auto! mb-4 mx-0!">
<TabTrigger :name="FieldInspectorTabs.Settings" :text="__('Settings')" />
<TabTrigger :name="FieldInspectorTabs.Conditions" :text="__('Logic')" />
<TabTrigger :name="FieldInspectorTabs.Logic" :text="__('Logic')" />
<TabTrigger v-if="shouldShowValidationTab" :name="FieldInspectorTabs.Validation" :text="__('Validation')" />
</TabList>

Expand Down Expand Up @@ -242,7 +242,7 @@ onMounted(() => load());
</div>
</TabContent>

<TabContent :name="FieldInspectorTabs.Conditions">
<TabContent :name="FieldInspectorTabs.Logic">
<div class="space-y-6 pt-8">
<div class="flex items-center gap-2.5">
<div class="size-4">
Expand All @@ -261,6 +261,7 @@ onMounted(() => load());
:config="field.config"
:suggestable-fields="suggestableConditionFields"
:allow-custom-conditions="false"
:show-always-hide-option="true"
size="sm"
@updated="updateFieldConditions"
>
Expand Down
6 changes: 3 additions & 3 deletions resources/js/components/forms/builder/PageInspector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { deletePage, dirty, fieldtypes, inspecting: page, pages } = injectBuilder

enum PageInspectorTabs {
Settings = 'settings',
Conditions = 'conditions',
Logic = 'logic',
}

const confirmingDelete = ref(false);
Expand Down Expand Up @@ -72,7 +72,7 @@ watch(() => page.value.rules, dirty, { deep: true });
<Tabs v-model:modelValue="activeTab" :unmount-on-hide="false">
<TabList class="inline-flex flex-wrap [&_button]:w-auto! mb-4 mx-0!">
<TabTrigger :name="PageInspectorTabs.Settings" :text="__('Settings')" />
<TabTrigger v-if="!isLastPage" :name="PageInspectorTabs.Conditions" :text="__('Logic')" />
<TabTrigger v-if="!isLastPage" :name="PageInspectorTabs.Logic" :text="__('Logic')" />
</TabList>

<TabContent :name="PageInspectorTabs.Settings">
Expand Down Expand Up @@ -106,7 +106,7 @@ watch(() => page.value.rules, dirty, { deep: true });
</div>
</TabContent>

<TabContent v-if="!isLastPage" :name="PageInspectorTabs.Conditions">
<TabContent v-if="!isLastPage" :name="PageInspectorTabs.Logic">
<div class="space-y-6 pt-8">
<div class="flex items-center gap-2.5">
<div class="size-4">
Expand Down
7 changes: 5 additions & 2 deletions resources/js/components/forms/logic/FieldLogic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AddLogicRuleButton from './AddLogicRuleButton.vue';
import FieldLogicRule from './FieldLogicRule.vue';
import { computed, nextTick, ref, watch } from 'vue';
import { categories, categoryColorClasses } from '@/components/forms/builder/categories';
import { KEYS } from '@/components/field-conditions/Constants.js';

const emit = defineEmits(['update:fields']);

Expand All @@ -17,13 +18,13 @@ const collapsed = ref([]);

const fieldsWithLogic = computed(() => {
return props.fields.filter(field => {
return field.if || field.unless || field.if_any || field.unless_any;
return field.hidden || KEYS.some(key => field[key] && Object.keys(field[key]).length > 0);
});
});

const fieldsWithoutLogic = computed(() => {
return props.fields.filter(field => {
return !field.if && !field.unless && !field.if_any && !field.unless_any;
return !field.hidden && !KEYS.some(key => field[key] && Object.keys(field[key]).length > 0);
});
});

Expand Down Expand Up @@ -63,6 +64,7 @@ const getFieldConfig = (field) => ({

const getConditionsConfig = (field) => ({
handle: field.handle,
hidden: field.hidden,
if: field.if,
unless: field.unless,
if_any: field.if_any,
Expand Down Expand Up @@ -100,6 +102,7 @@ const updateConditions = (fieldId, conditions) => {
field._id === fieldId
? {
...field,
hidden: conditions.hidden ?? field.hidden,
if: conditions.if || null,
unless: conditions.unless || null,
if_any: conditions.if_any || null,
Expand Down
3 changes: 2 additions & 1 deletion resources/js/components/forms/logic/FieldLogicRule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const previewParts = computed(() => {
});

const collapsedSummary = computed(() => {
if (!hasConditions.value) return __('Always show');
if (!hasConditions.value) return props.conditions.hidden ? __('Always hide') : __('Always show');
if (!previewParts.value) return __('Configure conditions');
return null;
});
Expand Down Expand Up @@ -185,6 +185,7 @@ const onAlwaysSaveUpdated = (alwaysSave) => emit('update:conditions', { ...props
:suggestable-fields
:fieldtypes
:allow-custom-conditions="false"
:show-always-hide-option="true"
:show-always-save="false"
size="sm"
@updated="onConditionsUpdated"
Expand Down
1 change: 1 addition & 0 deletions resources/js/pages/forms/Logic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const save = () => {
})),
fields: fields.value.map(field => ({
_id: field._id,
hidden: field.hidden,
if: field.if,
unless: field.unless,
if_any: field.if_any,
Expand Down
7 changes: 6 additions & 1 deletion src/Http/Controllers/CP/Forms/FormLogicController.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ private function fieldsToVue($formFields): array
'icon' => $fieldtype?->icon() ?? 'generic-field',
'category' => $fieldtype->categories()[0] ?? 'other',
'fieldtype' => $field['type'] ?? 'short_answer',
'hidden' => $field['hidden'] ?? false,
'if' => $field['if'] ?? null,
'unless' => $field['unless'] ?? null,
'if_any' => $field['if_any'] ?? null,
Expand Down Expand Up @@ -147,7 +148,11 @@ private function mergeLogicIntoForm(Request $request, Form $form): void
$conditions = $fieldConditions->get($handle);
$field = $fieldConfig['field'] ?? [];

unset($field['if'], $field['unless'], $field['if_any'], $field['unless_any'], $field['always_save']);
unset($field['hidden'], $field['if'], $field['unless'], $field['if_any'], $field['unless_any'], $field['always_save']);

if (! empty($conditions['hidden'])) {
$field['hidden'] = $conditions['hidden'];
}

if (! empty($conditions['if'])) {
$field['if'] = $conditions['if'];
Expand Down
70 changes: 70 additions & 0 deletions tests/Feature/Forms/FormLogicTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,76 @@ public function it_can_update_field_conditions()
$this->assertTrue($fields[1]['field']['always_save']);
}

#[Test]
public function it_can_update_the_hidden_state_of_a_field()
{
Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(true);

$this->setTestRoles(['test' => ['access cp', 'configure forms']]);
$user = User::make()->assignRole('test')->save();

$form = tap(Form::make('test')->formFields([
'pages' => [
[
'id' => 'page1',
'sections' => [
[
'display' => 'Section',
'fields' => [
[
'handle' => 'name',
'field' => [
'type' => 'short_answer',
'display' => 'Name',
'hidden' => true,
],
],
[
'handle' => 'email',
'field' => [
'type' => 'short_answer',
'display' => 'Email',
],
],
],
],
],
],
],
]))->save();

// The hidden state should be provided when loading the logic page.
$this
->actingAs($user)
->get(cp_route('forms.logic.edit', $form->handle()))
->assertSuccessful()
->assertInertia(fn ($page) => $page
->where('fields.0.hidden', true)
->where('fields.1.hidden', false)
);

$payload = [
'pages' => [],
'fields' => [
['_id' => 'name', 'hidden' => false],
['_id' => 'email', 'hidden' => true],
],
];

$this
->actingAs($user)
->patch(cp_route('forms.logic.update', $form->handle()), $payload)
->assertSuccessful();

$fields = Form::find('test')->formFields()->pages()[0]['sections'][0]['fields'];

// The now-visible field shouldn't persist `hidden` since false is the default.
$this->assertArrayNotHasKey('hidden', $fields[0]['field']);

// The now-hidden field should persist `hidden: true`.
$this->assertTrue($fields[1]['field']['hidden']);
}

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