Moved UI to client folder to fix HMR

This commit is contained in:
Stefan Zermatten
2022-11-19 17:51:50 +02:00
parent 060b5f93ca
commit e3644eb9e8
358 changed files with 1069 additions and 1066 deletions

View File

@@ -0,0 +1,80 @@
<template lang="html">
<div
v-if="computedErrors.length"
class="error-list"
>
<v-slide-x-transition
group
hide-on-leave
>
<v-alert
v-for="error in computedErrors"
:key="error.message"
:value="true"
:icon="errorIcon(error.type)"
:color="errorColor(error.type)"
class="mb-2"
dense
text
>
<pre>{{ error.message }}</pre>
</v-alert>
</v-slide-x-transition>
</div>
</template>
<script lang="js">
export default {
props: {
errors: {
type: Array,
default: undefined,
},
calculations: {
type: Array,
default: undefined,
},
},
computed: {
computedErrors(){
if (this.errors) {
return this.errors;
} else if (this.calculations){
let errors = [];
this.calculations.forEach(calc => {
if (calc.errors) errors.push(...calc.errors)
});
return errors;
} else {
return [];
}
},
},
methods: {
errorIcon(type){
if (type === 'subsitution'){
return 'mdi-information';
} else if (type === 'evaluation'){
return 'mdi-alert-circle';
} else {
return 'mdi-alert'
}
},
errorColor(type){
if (type === 'subsitution'){
return 'info';
} else if (type === 'evaluation'){
return 'warning';
} else {
return 'error'
}
},
}
}
</script>
<style lang="css">
.error-list .v-alert__content{
overflow-x: auto;
}
</style>

View File

@@ -0,0 +1,57 @@
<template lang="html">
<div class="computed-field">
<text-field
:value="model.calculation"
v-bind="$attrs"
@change="(value, ack) => $emit('change', {path: ['calculation'], value, ack})"
>
<template
v-if="showValue"
#value
>
{{ model.value }}
</template>
</text-field>
<calculation-error-list :errors="errorList" />
</div>
</template>
<script lang="js">
import CalculationErrorList from '/imports/client/ui/properties/forms/shared/CalculationErrorList.vue';
export default {
components: {
CalculationErrorList,
},
props: {
model: {
type: Object,
default: () => ({}),
},
hideValue: {
type: Boolean,
},
},
computed: {
showValue() {
const value = this.model.value;
if (
this.hideValue ||
(value === undefined || value === null) ||
value == this.model.calculation
) return false;
return true;
},
errorList(){
if (this.model.parseError){
return [this.model.parseError, ...this.model.errors];
} else {
return this.model.errors;
}
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,37 @@
<template lang="html">
<form-sections v-if="standalone">
<v-expansion-panel>
<v-expansion-panel-header>
{{ name }}
</v-expansion-panel-header>
<v-expansion-panel-content class="pt-2">
<slot />
</v-expansion-panel-content>
</v-expansion-panel>
</form-sections>
<v-expansion-panel v-else>
<v-expansion-panel-header>
{{ name }}
</v-expansion-panel-header>
<v-expansion-panel-content class="pt-2">
<slot />
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script lang="js">
import FormSections from '/imports/client/ui/properties/forms/shared/FormSections.vue';
export default {
components: {
FormSections,
},
props: {
name: {
type: String,
default: '',
},
standalone: Boolean,
},
}
export { FormSections };
</script>

View File

@@ -0,0 +1,12 @@
<template lang="html">
<v-expansion-panels
popout
multiple
>
<slot />
</v-expansion-panels>
</template>
<script lang="js">
export default {}
</script>

View File

@@ -0,0 +1,46 @@
<template lang="html">
<div
class="d-flex justify-center flex-wrap"
>
<div class="mx-1">
<color-picker
label="Color"
:value="model.color"
@input="value =>$emit('change', {path: ['color'], value})"
/>
</div>
<div class="mx-1">
<icon-picker
label="Icon"
:value="model.icon"
:error-messages="errors.icon"
@change="(value, ack) =>$emit('change', {path: ['icon'], value, ack})"
/>
</div>
</div>
</template>
<script lang="js">
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
import ColorPicker from '/imports/client/ui/components/ColorPicker.vue';
export default {
components: {
PropertyIcon,
ColorPicker,
},
props: {
model: {
type: Object,
required: true,
},
errors: {
type: Object,
required: true,
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,52 @@
<template lang="html">
<div class="inline-computation-field">
<text-area
:value="model.text"
v-bind="$attrs"
@change="(value, ack) => $emit('change', {path: ['text'], value, ack})"
/>
<template v-for="calc in model.inlineCalculations">
<div
v-if="calc && calc.calculation && (
(calc.errors && calc.errors.length) || calc.parseError
)"
:key="calc.calculation"
class="mb-4"
>
<div
class="warning--text mb-2 ml-4"
style="font-family: monospace;"
>
{ {{ calc.calculation }} }
</div>
<calculation-error-list
v-if="calc.parseError"
:errors="[calc.parseError]"
/>
<calculation-error-list
v-if="calc.errors && calc.errors.length"
:errors="calc.errors"
/>
</div>
</template>
</div>
</template>
<script lang="js">
import CalculationErrorList from '/imports/client/ui/properties/forms/shared/CalculationErrorList.vue';
export default {
components: {
CalculationErrorList,
},
props: {
model: {
type: Object,
default: () => ({}),
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,89 @@
<template lang="html">
<smart-select
append-icon="mdi-menu-down"
:clearable="clearable"
class="ml-3"
v-bind="$attrs"
:menu-props="{transition: 'slide-y-transition', lazy: true}"
:items="values"
:value="value"
@change="(value, ack) => $emit('change', value, ack)"
>
<v-icon
slot="prepend"
class="icon"
:class="iconClass"
>
{{ displayedIcon }}
</v-icon>
</smart-select>
</template>
<script lang="js">
import getProficiencyIcon from '/imports/client/ui/utility/getProficiencyIcon.js';
const ICON_SPIN_DURATION = 300;
export default {
props: {
value: {
type: Number,
default: undefined,
},
clearable: {
type: Boolean,
default: true,
},
},
data(){ return {
displayedIcon: 'mdi-radiobox-blank',
iconClass: '',
values: [
{value: 1, text: 'Proficient'},
{value: 0.49, text: 'Half proficiency bonus rounded down'},
{value: 0.5, text: 'Half proficiency bonus rounded up'},
{value: 2, text: 'Double proficiency bonus'},
],
}},
watch: {
'value': {
immediate: true,
handler(newValue){
let newIcon = getProficiencyIcon(newValue);
this.iconClass='leaving';
setTimeout(() => {
this.displayedIcon = newIcon;
this.iconClass='arriving';
requestAnimationFrame(() => {
this.iconClass='';
});
}, ICON_SPIN_DURATION / 2);
},
},
}
}
</script>
<style lang="css" scoped>
.theme--light .icon {
color: black;
}
.icon {
min-width: 30px;
transition: transform 0.15s linear, opacity 0.15s ease;
transform-origin: 18px center;
margin-left: -12px;
}
.icon.leaving {
transform: translateY(-24px);
opacity: 0;
}
.icon.arriving {
transform: translateY(24px);
opacity: 0;
transition: none;
}
.hidden {
visibility: hidden;
}
</style>

View File

@@ -0,0 +1,11 @@
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js';
const attributeListMixin = {
meteor: {
attributeList(){
return createListOfProperties({type: {$in: ['attribute', 'skill']}});
},
},
};
export default attributeListMixin;

View File

@@ -0,0 +1,23 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
export default function createListOfProperties(filter = {}, getNamesWithValues) {
filter.removed = { $ne: true };
let propertyList = [];
let variableNames = new Set();
function addUniquePropertys(property) {
if (property.variableName && !variableNames.has(property.variableName)) {
variableNames.add(property.variableName);
propertyList.push({
text: property.name || property.variableName,
value: property.variableName,
propertyType: property.propertyType,
});
}
}
let options = { sort: { order: 1, variableName: 1 } }
CreatureProperties.find(filter, options).forEach(addUniquePropertys);
LibraryNodes.find(filter, options).forEach(addUniquePropertys);
if (getNamesWithValues) return propertyList;
return Array.from(variableNames);
}

View File

@@ -0,0 +1,11 @@
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js'
const saveListMixin = {
meteor: {
saveList(){
return createListOfProperties({type: 'skill', skillType: 'save'});
},
},
};
export default saveListMixin;

View File

@@ -0,0 +1,11 @@
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js'
const skillListMixin = {
meteor: {
skillList(){
return createListOfProperties({type: 'skill'});
},
},
};
export default skillListMixin;

View File

@@ -0,0 +1,61 @@
import ActionForm from '/imports/client/ui/properties/forms/ActionForm.vue';
import AdjustmentForm from '/imports/client/ui/properties/forms/AdjustmentForm.vue';
import AttributeForm from '/imports/client/ui/properties/forms/AttributeForm.vue';
import BuffForm from '/imports/client/ui/properties/forms/BuffForm.vue';
import BuffRemoverForm from '/imports/client/ui/properties/forms/BuffRemoverForm.vue';
import BranchForm from '/imports/client/ui/properties/forms/BranchForm.vue';
import ClassForm from '/imports/client/ui/properties/forms/ClassForm.vue';
import ClassLevelForm from '/imports/client/ui/properties/forms/ClassLevelForm.vue';
import ConstantForm from '/imports/client/ui/properties/forms/ConstantForm.vue';
import ContainerForm from '/imports/client/ui/properties/forms/ContainerForm.vue';
import DamageForm from '/imports/client/ui/properties/forms/DamageForm.vue';
import DamageMultiplierForm from '/imports/client/ui/properties/forms/DamageMultiplierForm.vue';
import EffectForm from '/imports/client/ui/properties/forms/EffectForm.vue';
import FeatureForm from '/imports/client/ui/properties/forms/FeatureForm.vue';
import FolderForm from '/imports/client/ui/properties/forms/FolderForm.vue';
import ItemForm from '/imports/client/ui/properties/forms/ItemForm.vue';
import NoteForm from '/imports/client/ui/properties/forms/NoteForm.vue';
import PointBuyForm from '/imports/client/ui/properties/forms/PointBuyForm.vue';
import ProficiencyForm from '/imports/client/ui/properties/forms/ProficiencyForm.vue';
import ReferenceForm from '/imports/client/ui/properties/forms/ReferenceForm.vue';
import RollForm from '/imports/client/ui/properties/forms/RollForm.vue';
import SavingThrowForm from '/imports/client/ui/properties/forms/SavingThrowForm.vue';
import SkillForm from '/imports/client/ui/properties/forms/SkillForm.vue';
import SlotForm from '/imports/client/ui/properties/forms/SlotForm.vue';
import SlotFillerForm from '/imports/client/ui/properties/forms/SlotFillerForm.vue';
import SpellListForm from '/imports/client/ui/properties/forms/SpellListForm.vue';
import SpellForm from '/imports/client/ui/properties/forms/SpellForm.vue';
import ToggleForm from '/imports/client/ui/properties/forms/ToggleForm.vue';
import TriggerForm from '/imports/client/ui/properties/forms/TriggerForm.vue';
export default {
action: ActionForm,
adjustment: AdjustmentForm,
attribute: AttributeForm,
buff: BuffForm,
buffRemover: BuffRemoverForm,
branch: BranchForm,
constant: ConstantForm,
container: ContainerForm,
class: ClassForm,
classLevel: ClassLevelForm,
damage: DamageForm,
damageMultiplier: DamageMultiplierForm,
effect: EffectForm,
feature: FeatureForm,
folder: FolderForm,
item: ItemForm,
note: NoteForm,
pointBuy: PointBuyForm,
proficiency: ProficiencyForm,
propertySlot: SlotForm,
reference: ReferenceForm,
roll: RollForm,
savingThrow: SavingThrowForm,
skill: SkillForm,
slotFiller: SlotFillerForm,
spellList: SpellListForm,
spell: SpellForm,
toggle: ToggleForm,
trigger: TriggerForm,
};

View File

@@ -0,0 +1,40 @@
import ComputedField from '/imports/client/ui/properties/forms/shared/ComputedField.vue';
import InlineComputationField from '/imports/client/ui/properties/forms/shared/InlineComputationField.vue';
import FormSection, { FormSections } from '/imports/client/ui/properties/forms/shared/FormSection.vue';
export default {
components: {
ComputedField,
InlineComputationField,
FormSection,
FormSections,
},
props: {
model: {
type: [Object, Array],
default: () => ({}),
},
errors: {
type: Object,
default: () => ({}),
},
},
mounted(){
// Don't autofocus on mobile, it brings up the on-screen keyboard
if (this.$vuetify.breakpoint.smAndDown) return;
setTimeout(() => {
if (this.$refs.focusFirst && this.$refs.focusFirst.focus){
this.$refs.focusFirst.focus()
}
}, 300);
},
methods: {
change(path, value, ack){
if (!Array.isArray(path)){
path = [path];
}
this.$emit('change', {path, value, ack});
}
},
}

View File

@@ -0,0 +1,83 @@
/**
* Forms that take in a schema and a model of the current data, manages smart
* inputs, and sends update events when valid data model changes must occur
*/
import { get, toPath } from 'lodash';
function resolvePath(model, path, set) {
let arrayPath = toPath(path);
if (arrayPath.length === 1) {
return { object: model, key: arrayPath[0] };
}
let key = arrayPath.slice(-1);
let objectPath = arrayPath.slice(0, -1);
let object = model;
// Ensure that nested objects exist before navigating them
objectPath.forEach(pathKey => {
let newObject = object[pathKey];
if (!newObject) {
newObject = {};
set(object, pathKey, newObject);
}
object = newObject;
});
return { object, key };
}
const schemaFormMixin = {
data() {
return {
valid: true,
};
},
computed: {
errors() {
this.valid = true;
if (!this.model) {
throw new Error('this.model must be set');
}
if (!this.validationContext) return {};
let cleanModel = this.validationContext.clean(this.model, {
getAutoValues: false,
});
this.validationContext.validate(cleanModel);
let errors = {};
this.validationContext.validationErrors().forEach(error => {
if (this.valid) this.valid = false;
errors[error.name] = this.schema.messageForError(error);
});
return errors;
},
},
methods: {
// Sets the value at the given path
change({ path, value, ack }) {
let { object, key } = resolvePath(this.model, path, this.$set);
this.$set(object, key, value);
if (ack) ack();
},
push({ path, value, ack }) {
let array = get(this.model, path);
if (array === undefined) {
let { object, key } = resolvePath(this.model, path, this.$set);
this.$set(object, key, [value]);
} else if (!array.push) {
throw `${path.join('.')} is ${array}, doesn't have "push"`
} else {
array.push(value);
}
if (ack) ack();
},
pull({ path, ack }) {
let { object, key } = resolvePath(this.model, path, this.$set);
if (!object || !object.splice) {
throw `${path.join('.')} is ${object}, doesnt have "splice"`
}
object.splice(key, 1);
if (ack) ack();
},
},
};
export default schemaFormMixin;