Skip to content
4 changes: 4 additions & 0 deletions resources/js/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@
FieldGeoCoordinates: () => import(/* webpackChunkName: "field-geo-coordinates" */'app/components/form/field-geo-coordinates'),
FieldInteger: () => import(/* webpackChunkName: "field-integer" */'app/components/form/field-integer'),
FieldJson: () => import(/* webpackChunkName: "field-json" */'app/components/form/field-json'),
FieldMultipleCheckboxes: () => import(/* webpackChunkName: "field-multiple-checkboxes" */'app/components/form/field-multiple-checkboxes'),
FieldNumber: () => import(/* webpackChunkName: "field-number" */'app/components/form/field-number'),
FieldPassword: () => import(/* webpackChunkName: "field-password" */'app/components/form/field-password'),
FieldPlaintext: () => import(/* webpackChunkName: "field-plaintext" */'app/components/form/field-plaintext'),
FieldRadio: () => import(/* webpackChunkName: "field-radio" */'app/components/form/field-radio'),
FieldSelect: () => import(/* webpackChunkName: "field-select" */'app/components/form/field-select'),
Expand Down Expand Up @@ -632,7 +634,9 @@
Vue.component('FieldDate', _vueInstance.$options.components.FieldDate);
Vue.component('FieldInteger', _vueInstance.$options.components.FieldInteger);
Vue.component('FieldJson', _vueInstance.$options.components.FieldJson);
Vue.component('FieldMultipleCheckboxes', _vueInstance.$options.components.FieldMultipleCheckboxes);
Vue.component('FieldNumber', _vueInstance.$options.components.FieldNumber);
Vue.component('FieldPassword', _vueInstance.$options.components.FieldPassword);
Vue.component('FieldPlaintext', _vueInstance.$options.components.FieldPlaintext);
Vue.component('FieldRadio', _vueInstance.$options.components.FieldRadio);
Vue.component('FieldSelect', _vueInstance.$options.components.FieldSelect);
Expand All @@ -646,6 +650,6 @@
Vue.component('ObjectCaptions', _vueInstance.$options.components.ObjectCaptions);
Vue.component('ObjectInfo', _vueInstance.$options.components.ObjectInfo);
Vue.component('RelatedObjectsFilter', _vueInstance.$options.components.RelatedObjectsFilter);
Vue.component('Thumbnail', _vueInstance.$options.components.Thumbnail);

Check warning on line 653 in resources/js/app/app.js

View workflow job for this annotation

GitHub Actions / Check javascript build (20.x)

Component name "Thumbnail" should always be multi-word

Check warning on line 653 in resources/js/app/app.js

View workflow job for this annotation

GitHub Actions / Check javascript build (22.x)

Component name "Thumbnail" should always be multi-word

Check warning on line 653 in resources/js/app/app.js

View workflow job for this annotation

GitHub Actions / Check javascript build (24.x)

Component name "Thumbnail" should always be multi-word
Vue.component('RibbonItem', _vueInstance.$options.components.RibbonItem);
Vue.component('UploadedObject', _vueInstance.$options.components.UploadedObject);
24 changes: 23 additions & 1 deletion resources/js/app/components/fast-create/form-field.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,23 @@
@change="update"
v-if="fieldType === 'geo-coordinates'"
/>
<!-- input password -->
<template v-if="fieldType === 'string' && field === 'password'">
<field-password
:id="`fast-create-${field}`"
:name="`fast-${objectType}-${field}`"
:reference="`fast-create-${objectType}`"
@change="update"
/>
</template>
<!-- input text -->
<field-string
:id="`fast-create-${field}`"
:name="`fast-${objectType}-${field}`"
:reference="`fast-create-${objectType}`"
:value="value"
@change="update"
v-if="fieldType === 'string'"
v-if="fieldType === 'string' && field !== 'password'"
/>
<!-- captions -->
<object-captions
Expand All @@ -142,6 +151,16 @@
@change="update"
v-if="fieldType === 'json'"
/>
<!-- multiple checkboxes -->
<field-multiple-checkboxes
:id="`fast-create-${field}`"
:json-schema="jsonSchema"
:name="`fast-${objectType}-${field}`"
:reference="`fast-create-${objectType}`"
:value="value"
@change="update"
v-if="fieldType === 'multiple-checkboxes'"
/>
</div>
</div>
</template>
Expand Down Expand Up @@ -228,6 +247,9 @@ export default {
if (this.field === 'date_ranges') {
return 'calendar';
}
if (this.jsonSchema?.type === 'array' && this.jsonSchema?.items?.type === 'string' && this.jsonSchema?.items?.enum?.length > 0) {
return 'multiple-checkboxes';
}
if (this.field === 'extra' || ['array','object'].includes(this.jsonSchema?.type) || this.jsonSchema?.oneOf?.filter(one => ['array','object'].includes(one?.type))?.length > 0) {
return 'json';
}
Expand Down
93 changes: 93 additions & 0 deletions resources/js/app/components/form/field-multiple-checkboxes.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<template>
<div class="field-multiple-checkboxes">
<label
v-for="item in items"
:key="item"
>
<input
:id="`field-multiple-checkboxes-${item}`"
:name="item"
:data-ref="reference"
class="field-checkbox"
type="checkbox"
:checked="v.includes(item)"
@change="update($event.target)"
>
<span>{{ item }}</span>
</label>
<input
:id="id"
:name="name"
:data-ref="reference"
type="hidden"
v-model="serialized"
@change="$emit('change', serialized)"
>
</div>
</template>
<script>
export default {
name: 'FieldMultipleCheckboxes',
props: {
id: {
type: String,
required: true
},
jsonSchema: {
type: Object,
required: true,
},
name: {
type: String,
required: true
},
reference: {
type: String,
default: ''
},
value: {
type: String,
default: ''
},
},
data() {
return {
items: this.jsonSchema?.items?.enum || [],
serialized: JSON.stringify(this.value || []),
v: this.value || [],
};
},
mounted() {
this.$nextTick(() => {
this.items.sort();
this.v.sort();
});
},
methods: {
update(target) {
const val = target.checked;
const targetName = target.name;
if (val) {
this.v.push(targetName);
} else {
this.v = this.v.filter(i => i !== targetName);
}
this.v.sort();
this.serialized = JSON.stringify(this.v);
this.$emit('change', this.serialized);
}
},
}
</script>
<style scoped>
div.field-multiple-checkboxes {
display: flex;
flex-direction: column;
}
div.field-multiple-checkboxes > label {
cursor: pointer;
}
div.field-multiple-checkboxes > label:hover {
text-decoration: underline;
}
</style>
91 changes: 91 additions & 0 deletions resources/js/app/components/form/field-password.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<div class="field-password">
<input
:id="id"
:name="name"
:data-ref="reference"
autocomplete="new-password"
class="password"
data-original-value=""
:placeholder="getI18n('password', 'password')"
type="password"
value=""
v-model="password"
@change="change($event.target.value)"
>
<input
:id="confirmPasswordField"
:name="confirmPasswordField"
autocomplete="new-password"
class="password"
data-original-value=""
:placeholder="getI18n('confirm-password', 'confirm password')"
type="password"
value=""
v-model="confirmPassword"
@change="change($event.target.value)"
>
<span v-if="password && password === confirmPassword">
<app-icon
icon="carbon:checkmark"
color="green"
/>
</span>
<span v-if="password !== confirmPassword">
<app-icon
icon="carbon:misuse"
color="red"
/>
</span>
</div>
</template>
<script>
export default {
name: 'FieldPassword',
props: {
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
reference: {
type: String,
default: ''
},
value: {
type: String,
default: ''
},
},
data() {
return {
confirmPassword: '',
confirmPasswordField: 'confirm-password',
confirmPasswordTitle: 'Confirm Password',
password: '',
};
},
methods: {
change(value) {
if (this.confirmPassword && this.password !== this.confirmPassword) {
return;
}
this.$emit('change', value);
},
getI18n(field, defaultValue) {
return BEDITA_I18N?.[field] || defaultValue;
},
},
}
</script>
<style scoped>
div.field-password {
display: grid;
grid-template-columns: 1fr 1fr 50px;
gap: 10px;
align-items: center;
}
</style>
28 changes: 25 additions & 3 deletions src/Controller/AppController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use App\Form\Form;
use App\Utility\DateRangesTools;
use App\Utility\PermissionsTrait;
use Authentication\Identity;
use BEdita\WebTools\ApiClientProvider;
use Cake\Controller\Controller;
Expand All @@ -36,6 +37,8 @@
*/
class AppController extends Controller
{
use PermissionsTrait;

/**
* BEdita4 API client
*
Expand Down Expand Up @@ -234,7 +237,7 @@ protected function specialAttributes(array &$data): void
}

$this->decodeJsonAttributes($data);

$this->prepareRoles($data);
$this->prepareDateRanges($data);

// prepare categories
Expand Down Expand Up @@ -299,6 +302,25 @@ protected function prepareDateRanges(array &$data): void
$data['date_ranges'] = DateRangesTools::prepare(Hash::get($data, 'date_ranges'));
}

/**
* Transform roles data into relations format.
*
* @param array $data The data to prepare
* @return void
*/
protected function prepareRoles(array &$data): void
{
$roles = (string)Hash::get($data, 'roles');
$roles = empty($roles) ? [] : (array)json_decode($roles, true);
$data = array_filter($data, fn($key) => $key !== 'roles', ARRAY_FILTER_USE_KEY);
if (!empty($roles)) {
$data['relations']['roles']['replaceRelated'] = array_map(
fn($id) => ['id' => $id, 'type' => 'roles'],
array_keys($this->rolesByNames($roles))
);
}
}

/**
* Prepare request relation data.
*
Expand All @@ -311,7 +333,7 @@ protected function prepareRelations(array &$data): void
if (!empty($data['relations'])) {
$api = [];
foreach ($data['relations'] as $relation => $relationData) {
$id = $data['id'];
$id = Hash::get($data, 'id', null);
foreach ($relationData as $method => $ids) {
$relatedIds = $this->relatedIds($ids);
if ($method === 'replaceRelated' || !empty($relatedIds)) {
Expand All @@ -321,7 +343,7 @@ protected function prepareRelations(array &$data): void
}
$data['_api'] = $api;
}
unset($data['relations']);
$data = array_filter($data, fn($key) => $key !== 'relations', ARRAY_FILTER_USE_KEY);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Controller/Component/ModulesComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ public function skipSaveRelated(string $id, array &$relatedData): bool
return true;
}
$methods = (array)Hash::extract($relatedData, '{n}.method');
if (in_array('addRelated', $methods) || in_array('removeRelated', $methods)) {
if (in_array('addRelated', $methods) || in_array('removeRelated', $methods) || empty($id)) {
return false;
}
// check replaceRelated
Expand Down
8 changes: 6 additions & 2 deletions src/Controller/Component/SchemaComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,12 @@ protected function fetchSchema(string $type)
// add special property `roles` to `users`
if ($type === 'users') {
$schema['properties']['roles'] = [
'type' => 'string',
'enum' => $this->fetchRoles(),
'type' => 'array',
'items' => [
'type' => 'string',
'enum' => $this->fetchRoles(),
],
'uniqueItems' => true,
];
}
$categories = $this->fetchCategories($type);
Expand Down
2 changes: 0 additions & 2 deletions src/Controller/ModulesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use App\Utility\ApiConfigTrait;
use App\Utility\CacheTools;
use App\Utility\Message;
use App\Utility\PermissionsTrait;
use BEdita\SDK\BEditaClientException;
use BEdita\WebTools\Utility\ApiTools;
use Cake\Core\Configure;
Expand Down Expand Up @@ -45,7 +44,6 @@
class ModulesController extends AppController
{
use ApiConfigTrait;
use PermissionsTrait;

/**
* Object type currently used
Expand Down
Loading
Loading