mirror of
https://github.com/RoY7x/worldbuilding.git
synced 2025-04-30 10:41:40 -04:00
Merge branch '9-actor-templates-integration' into 'master'
9: Add actor templates See merge request foundrynet/worldbuilding!8
This commit is contained in:
commit
c515f8d5b7
10 changed files with 270 additions and 85 deletions
|
@ -9,5 +9,9 @@
|
||||||
|
|
||||||
"SIMPLE.ResourceMin": "Min",
|
"SIMPLE.ResourceMin": "Min",
|
||||||
"SIMPLE.ResourceValue": "Value",
|
"SIMPLE.ResourceValue": "Value",
|
||||||
"SIMPLE.ResourceMax": "Max"
|
"SIMPLE.ResourceMax": "Max",
|
||||||
|
|
||||||
|
"SIMPLE.DefineTemplate": "Define as Template",
|
||||||
|
"SIMPLE.UnsetTemplate": "Unset Template",
|
||||||
|
"SIMPLE.NoTemplate": "No Template"
|
||||||
}
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ATTRIBUTE_TYPES } from "./constants.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the basic ActorSheet with some very simple modifications
|
* Extend the basic ActorSheet with some very simple modifications
|
||||||
* @extends {ActorSheet}
|
* @extends {ActorSheet}
|
||||||
|
@ -21,7 +23,7 @@ export class SimpleActorSheet extends ActorSheet {
|
||||||
/** @override */
|
/** @override */
|
||||||
getData() {
|
getData() {
|
||||||
const data = super.getData();
|
const data = super.getData();
|
||||||
data.dtypes = ["String", "Number", "Boolean", "Formula", "Resource"];
|
data.dtypes = ATTRIBUTE_TYPES;
|
||||||
for ( let attr of Object.values(data.data.attributes) ) {
|
for ( let attr of Object.values(data.data.attributes) ) {
|
||||||
attr.isCheckbox = attr.dtype === "Boolean";
|
attr.isCheckbox = attr.dtype === "Boolean";
|
||||||
attr.isResource = attr.dtype === "Resource";
|
attr.isResource = attr.dtype === "Resource";
|
||||||
|
|
1
module/constants.js
Normal file
1
module/constants.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const ATTRIBUTE_TYPES = ["String", "Number", "Boolean", "Formula", "Resource"];
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ATTRIBUTE_TYPES } from "./constants.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the basic ItemSheet with some very simple modifications
|
* Extend the basic ItemSheet with some very simple modifications
|
||||||
* @extends {ItemSheet}
|
* @extends {ItemSheet}
|
||||||
|
@ -20,7 +22,7 @@ export class SimpleItemSheet extends ItemSheet {
|
||||||
/** @override */
|
/** @override */
|
||||||
getData() {
|
getData() {
|
||||||
const data = super.getData();
|
const data = super.getData();
|
||||||
data.dtypes = ["String", "Number", "Boolean", "Formula", "Resource"];
|
data.dtypes = ATTRIBUTE_TYPES;
|
||||||
for ( let attr of Object.values(data.data.attributes) ) {
|
for ( let attr of Object.values(data.data.attributes) ) {
|
||||||
attr.isCheckbox = attr.dtype === "Boolean";
|
attr.isCheckbox = attr.dtype === "Boolean";
|
||||||
attr.isResource = attr.dtype === "Resource";
|
attr.isResource = attr.dtype === "Resource";
|
||||||
|
|
181
module/simple.js
181
module/simple.js
|
@ -8,6 +8,7 @@
|
||||||
import { SimpleActor } from "./actor.js";
|
import { SimpleActor } from "./actor.js";
|
||||||
import { SimpleItemSheet } from "./item-sheet.js";
|
import { SimpleItemSheet } from "./item-sheet.js";
|
||||||
import { SimpleActorSheet } from "./actor-sheet.js";
|
import { SimpleActorSheet } from "./actor-sheet.js";
|
||||||
|
import { preloadHandlebarsTemplates } from "./templates.js";
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Foundry VTT Initialization */
|
/* Foundry VTT Initialization */
|
||||||
|
@ -89,4 +90,184 @@ Hooks.once("init", async function() {
|
||||||
return value.slugify({strict: true});
|
return value.slugify({strict: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Preload template partials.
|
||||||
|
preloadHandlebarsTemplates();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the actor template context menu.
|
||||||
|
*/
|
||||||
|
Hooks.on("getActorDirectoryEntryContext", (html, options) => {
|
||||||
|
// Define an actor as a template.
|
||||||
|
options.push({
|
||||||
|
name: game.i18n.localize("SIMPLE.DefineTemplate"),
|
||||||
|
icon: '<i class="fas fa-stamp"></i>',
|
||||||
|
condition: li => {
|
||||||
|
const actor = game.actors.get(li.data("entityId"));
|
||||||
|
return !actor.getFlag("worldbuilding", "isTemplate");
|
||||||
|
},
|
||||||
|
callback: li => {
|
||||||
|
const actor = game.actors.get(li.data("entityId"));
|
||||||
|
actor.setFlag("worldbuilding", "isTemplate", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Undefine an actor as a template.
|
||||||
|
options.push({
|
||||||
|
name: game.i18n.localize("SIMPLE.UnsetTemplate"),
|
||||||
|
icon: '<i class="fas fa-times"></i>',
|
||||||
|
condition: li => {
|
||||||
|
const actor = game.actors.get(li.data("entityId"));
|
||||||
|
return actor.getFlag("worldbuilding", "isTemplate");
|
||||||
|
},
|
||||||
|
callback: li => {
|
||||||
|
const actor = game.actors.get(li.data("entityId"));
|
||||||
|
actor.setFlag("worldbuilding", "isTemplate", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the item template context menu.
|
||||||
|
*/
|
||||||
|
Hooks.on("getItemDirectoryEntryContext", (html, options) => {
|
||||||
|
// Define an item as a template.
|
||||||
|
options.push({
|
||||||
|
name: game.i18n.localize("SIMPLE.DefineTemplate"),
|
||||||
|
icon: '<i class="fas fa-stamp"></i>',
|
||||||
|
condition: li => {
|
||||||
|
const item = game.items.get(li.data("entityId"));
|
||||||
|
return !item.getFlag("worldbuilding", "isTemplate");
|
||||||
|
},
|
||||||
|
callback: li => {
|
||||||
|
const item = game.items.get(li.data("entityId"));
|
||||||
|
item.setFlag("worldbuilding", "isTemplate", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Undefine an item as a template.
|
||||||
|
options.push({
|
||||||
|
name: game.i18n.localize("SIMPLE.UnsetTemplate"),
|
||||||
|
icon: '<i class="fas fa-times"></i>',
|
||||||
|
condition: li => {
|
||||||
|
const item = game.items.get(li.data("entityId"));
|
||||||
|
return item.getFlag("worldbuilding", "isTemplate");
|
||||||
|
},
|
||||||
|
callback: li => {
|
||||||
|
const item = game.items.get(li.data("entityId"));
|
||||||
|
item.setFlag("worldbuilding", "isTemplate", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the actor template selection dialog.
|
||||||
|
*/
|
||||||
|
ActorDirectory.prototype._onCreate = async (event) => {
|
||||||
|
// Do not allow the creation event to bubble to other listeners
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
_simpleDirectoryTemplates('actor');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the item template selection dialog.
|
||||||
|
*/
|
||||||
|
ItemDirectory.prototype._onCreate = async (event) => {
|
||||||
|
// Do not allow the creation event to bubble to other listeners
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
_simpleDirectoryTemplates('item');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the entity template dialog.
|
||||||
|
*
|
||||||
|
* Helper function to display a dialog if there are multiple template types
|
||||||
|
* defined for the entity type.
|
||||||
|
*
|
||||||
|
* @param {string} entityType - 'actor' or 'item'
|
||||||
|
*/
|
||||||
|
async function _simpleDirectoryTemplates(entityType = 'actor') {
|
||||||
|
// Retrieve the collection and class.
|
||||||
|
const entityCollection = entityType == 'actor' ? game.actors : game.items;
|
||||||
|
const cls = entityType == 'actor' ? Actor : Item;
|
||||||
|
|
||||||
|
// Query for all entities of this type using the "isTemplate" flag.
|
||||||
|
let entities = entityCollection.filter(a => a.data.flags?.worldbuilding?.isTemplate === true);
|
||||||
|
|
||||||
|
// Initialize variables related to the entity class.
|
||||||
|
let ent = game.i18n.localize(cls.config.label);
|
||||||
|
|
||||||
|
// Setup entity data.
|
||||||
|
let type = entityType == 'actor' ? 'character' : 'item';
|
||||||
|
let createData = {
|
||||||
|
name: `New ${ent}`,
|
||||||
|
type: type,
|
||||||
|
folder: event.currentTarget.dataset.folder
|
||||||
|
};
|
||||||
|
|
||||||
|
// If there's more than one entity template type, create a form.
|
||||||
|
if (entities.length > 0) {
|
||||||
|
// Build an array of types for the form, including an empty default.
|
||||||
|
let types = [{
|
||||||
|
value: null,
|
||||||
|
label: game.i18n.localize("SIMPLE.NoTemplate")
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Append each of the user-defined actor/item types.
|
||||||
|
types = types.concat(entities.map(a => {
|
||||||
|
return {
|
||||||
|
value: a.data.name,
|
||||||
|
label: a.data.name
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Render the entity creation form
|
||||||
|
let templateData = {upper: ent, lower: ent.toLowerCase(), types: types},
|
||||||
|
dlg = await renderTemplate(`systems/worldbuilding/templates/sidebar/entity-create.html`, templateData);
|
||||||
|
|
||||||
|
// Render the confirmation dialog window
|
||||||
|
new Dialog({
|
||||||
|
title: `Create ${createData.name}`,
|
||||||
|
content: dlg,
|
||||||
|
buttons: {
|
||||||
|
create: {
|
||||||
|
icon: '<i class="fas fa-check"></i>',
|
||||||
|
label: `Create ${ent}`,
|
||||||
|
callback: html => {
|
||||||
|
// Get the form data.
|
||||||
|
const form = html[0].querySelector("form");
|
||||||
|
mergeObject(createData, validateForm(form));
|
||||||
|
|
||||||
|
// Store the type and name values, and retrieve the template entity.
|
||||||
|
let templateActor = entityCollection.getName(createData.type);
|
||||||
|
|
||||||
|
// If there's a template entity, handle the data.
|
||||||
|
if (templateActor) {
|
||||||
|
// Update the object with the existing template's values.
|
||||||
|
createData = mergeObject(templateActor.data, createData, {inplace: false});
|
||||||
|
createData.type = templateActor.data.type;
|
||||||
|
// Clear the flag so that this doesn't become a new template.
|
||||||
|
delete createData.flags.worldbuilding.isTemplate;
|
||||||
|
}
|
||||||
|
// Otherwise, restore to a valid entity type (character/item).
|
||||||
|
else {
|
||||||
|
createData.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.create(createData, {renderSheet: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
default: "create"
|
||||||
|
}).render(true);
|
||||||
|
}
|
||||||
|
// Otherwise, just create a blank entity.
|
||||||
|
else {
|
||||||
|
cls.create(createData, {renderSheet: true});
|
||||||
|
}
|
||||||
|
}
|
16
module/templates.js
Normal file
16
module/templates.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Define a set of template paths to pre-load
|
||||||
|
* Pre-loaded templates are compiled and cached for fast access when rendering
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export const preloadHandlebarsTemplates = async function() {
|
||||||
|
|
||||||
|
// Define template paths to load
|
||||||
|
const templatePaths = [
|
||||||
|
// Attribute list partial.
|
||||||
|
"systems/worldbuilding/templates/parts/sheet-attributes.html"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load the template parts
|
||||||
|
return loadTemplates(templatePaths);
|
||||||
|
};
|
|
@ -73,47 +73,8 @@
|
||||||
<a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a>
|
<a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ol class="attributes-list">
|
{{!-- Render the attribute list partial. --}}
|
||||||
{{#each data.attributes as |attr key|}}
|
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.attributes dtypes=dtypes}}
|
||||||
<li class="attribute flexrow" data-attribute="{{key}}">
|
|
||||||
<input class="attribute-key" type="text" name="data.attributes.{{key}}.key" value="{{key}}"/>
|
|
||||||
{{!-- Handle booleans. --}}
|
|
||||||
{{#if attr.isCheckbox}}
|
|
||||||
<label class="attribute-value checkbox"><input type="checkbox" name="data.attributes.{{key}}.value" {{checked attr.value}}/></label>
|
|
||||||
{{else}}
|
|
||||||
{{!-- Handle resources. --}}
|
|
||||||
{{#if attr.isResource}}
|
|
||||||
<div class="attribute-group flexrow">
|
|
||||||
<span class="attribute-col flexcol">
|
|
||||||
<label for="data.attributes.{{key}}.min">{{localize "SIMPLE.ResourceMin"}}</label>
|
|
||||||
<input class="attribute-value" type="text" name="data.attributes.{{key}}.min" value="{{attr.min}}" data-dtype="{{attr.dtype}}"/>
|
|
||||||
</span>
|
|
||||||
<span class="attribute-col flexcol">
|
|
||||||
<label for="data.attributes.{{key}}.value">{{localize "SIMPLE.ResourceValue"}}</label>
|
|
||||||
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
|
|
||||||
</span>
|
|
||||||
<span class="attribute-col flexcol">
|
|
||||||
<label for="data.attributes.{{key}}.max">{{localize "SIMPLE.ResourceMax"}}</label>
|
|
||||||
<input class="attribute-value" type="text" name="data.attributes.{{key}}.max" value="{{attr.max}}" data-dtype="{{attr.dtype}}"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{!-- Handle other input types. --}}
|
|
||||||
{{else}}
|
|
||||||
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
<input class="attribute-label" type="text" name="data.attributes.{{key}}.label" value="{{attr.label}}"/>
|
|
||||||
<select class="attribute-dtype" name="data.attributes.{{key}}.dtype">
|
|
||||||
{{#select attr.dtype}}
|
|
||||||
{{#each ../dtypes as |t|}}
|
|
||||||
<option value="{{t}}">{{t}}</option>
|
|
||||||
{{/each}}
|
|
||||||
{{/select}}
|
|
||||||
</select>
|
|
||||||
<a class="attribute-control" data-action="delete"><i class="fas fa-trash"></i></a>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -38,47 +38,8 @@
|
||||||
<a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a>
|
<a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ol class="attributes-list">
|
{{!-- Render the attribute list partial. --}}
|
||||||
{{#each data.attributes as |attr key|}}
|
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.attributes dtypes=dtypes}}
|
||||||
<li class="attribute flexrow" data-attribute="{{key}}">
|
|
||||||
<input class="attribute-key" type="text" name="data.attributes.{{key}}.key" value="{{key}}"/>
|
|
||||||
{{!-- Handle booleans. --}}
|
|
||||||
{{#if attr.isCheckbox}}
|
|
||||||
<label class="attribute-value checkbox"><input type="checkbox" name="data.attributes.{{key}}.value" {{checked attr.value}}/></label>
|
|
||||||
{{else}}
|
|
||||||
{{!-- Handle resources. --}}
|
|
||||||
{{#if attr.isResource}}
|
|
||||||
<div class="attribute-group flexrow">
|
|
||||||
<span class="attribute-col flexcol">
|
|
||||||
<label for="data.attributes.{{key}}.min">Min</label>
|
|
||||||
<input class="attribute-value" type="text" name="data.attributes.{{key}}.min" value="{{attr.min}}" data-dtype="{{attr.dtype}}"/>
|
|
||||||
</span>
|
|
||||||
<span class="attribute-col flexcol">
|
|
||||||
<label for="data.attributes.{{key}}.value">Current</label>
|
|
||||||
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
|
|
||||||
</span>
|
|
||||||
<span class="attribute-col flexcol">
|
|
||||||
<label for="data.attributes.{{key}}.max">Max</label>
|
|
||||||
<input class="attribute-value" type="text" name="data.attributes.{{key}}.max" value="{{attr.max}}" data-dtype="{{attr.dtype}}"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{!-- Handle other input types. --}}
|
|
||||||
{{else}}
|
|
||||||
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
<input class="attribute-label" type="text" name="data.attributes.{{key}}.label" value="{{attr.label}}"/>
|
|
||||||
<select class="attribute-dtype" name="data.attributes.{{key}}.dtype">
|
|
||||||
{{#select attr.dtype}}
|
|
||||||
{{#each ../dtypes as |t|}}
|
|
||||||
<option value="{{t}}">{{t}}</option>
|
|
||||||
{{/each}}
|
|
||||||
{{/select}}
|
|
||||||
</select>
|
|
||||||
<a class="attribute-control" data-action="delete"><i class="fas fa-trash"></i></a>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
|
|
41
templates/parts/sheet-attributes.html
Normal file
41
templates/parts/sheet-attributes.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<ol class="attributes-list">
|
||||||
|
{{#each attributes as |attr key|}}
|
||||||
|
<li class="attribute flexrow" data-attribute="{{key}}">
|
||||||
|
<input class="attribute-key" type="text" name="data.attributes.{{key}}.key" value="{{key}}"/>
|
||||||
|
{{!-- Handle booleans. --}}
|
||||||
|
{{#if attr.isCheckbox}}
|
||||||
|
<label class="attribute-value checkbox"><input type="checkbox" name="data.attributes.{{key}}.value" {{checked attr.value}}/></label>
|
||||||
|
{{else}}
|
||||||
|
{{!-- Handle resources. --}}
|
||||||
|
{{#if attr.isResource}}
|
||||||
|
<div class="attribute-group flexrow">
|
||||||
|
<span class="attribute-col flexcol">
|
||||||
|
<label for="data.attributes.{{key}}.min">{{localize "SIMPLE.ResourceMin"}}</label>
|
||||||
|
<input class="attribute-value" type="text" name="data.attributes.{{key}}.min" value="{{attr.min}}" data-dtype="{{attr.dtype}}"/>
|
||||||
|
</span>
|
||||||
|
<span class="attribute-col flexcol">
|
||||||
|
<label for="data.attributes.{{key}}.value">{{localize "SIMPLE.ResourceValue"}}</label>
|
||||||
|
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
|
||||||
|
</span>
|
||||||
|
<span class="attribute-col flexcol">
|
||||||
|
<label for="data.attributes.{{key}}.max">{{localize "SIMPLE.ResourceMax"}}</label>
|
||||||
|
<input class="attribute-value" type="text" name="data.attributes.{{key}}.max" value="{{attr.max}}" data-dtype="{{attr.dtype}}"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{!-- Handle other input types. --}}
|
||||||
|
{{else}}
|
||||||
|
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
<input class="attribute-label" type="text" name="data.attributes.{{key}}.label" value="{{attr.label}}"/>
|
||||||
|
<select class="attribute-dtype" name="data.attributes.{{key}}.dtype">
|
||||||
|
{{#select attr.dtype}}
|
||||||
|
{{#each ../dtypes as |t|}}
|
||||||
|
<option value="{{t}}">{{t}}</option>
|
||||||
|
{{/each}}
|
||||||
|
{{/select}}
|
||||||
|
</select>
|
||||||
|
<a class="attribute-control" data-action="delete"><i class="fas fa-trash"></i></a>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ol>
|
16
templates/sidebar/entity-create.html
Normal file
16
templates/sidebar/entity-create.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<form id="entity-create" autocomplete="off" onsubmit="event.preventDefault();">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "Name"}}</label>
|
||||||
|
<input type="text" name="name" placeholder="{{localize 'ENTITY.CreateNew'}} {{upper}}"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "Type"}}</label>
|
||||||
|
<select name="type">
|
||||||
|
{{#each types}}
|
||||||
|
<option value="{{this.value}}">{{this.label}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
<p class="notes">{{localize "ENTITY.TypeHint"}}</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
Loading…
Add table
Reference in a new issue