Compare commits

...

23 Commits

Author SHA1 Message Date
Stefan Zermatten
0e5bf39958 Bumped version 2023-07-21 16:18:50 +02:00
Stefan Zermatten
5d8485123e Merge branch 'develop' 2023-07-21 16:17:42 +02:00
Stefan Zermatten
85f13713f2 Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2023-07-21 16:14:14 +02:00
Stefan Zermatten
b0afc86ad4 "fixed" column layout again
As yet untested on Safari
2023-07-21 16:12:50 +02:00
Stefan Zermatten
30fabce7f1 Removed variables object from creature docs 2023-07-20 11:13:57 +02:00
Stefan Zermatten
4133a0f78c Fixed proficiency bonus not applying in actions 2023-07-19 19:40:59 +02:00
Stefan Zermatten
2b1a6de1e5 Relaxed rate limiting on duplicating library props 2023-07-19 19:03:36 +02:00
Stefan Zermatten
25e2523d51 Fixed resources in folder increment button loading forever 2023-07-19 18:57:06 +02:00
Stefan Zermatten
7072e9ba97 Fixed searching by tag in slot fillers 2023-07-19 18:49:18 +02:00
Stefan Zermatten
b3ed77964f Hotfix library select turning into links incorrectly 2023-06-28 10:22:38 +02:00
Stefan Zermatten
912fff64a8 Bumped version 2023-06-27 09:33:22 +02:00
Stefan Zermatten
22d51eacab Added second library tree for multi-track drifting 2023-06-26 16:31:23 +02:00
Stefan Zermatten
7562e29fac Increased power of tree searching 2023-06-26 14:45:19 +02:00
Stefan Zermatten
d4cac831e6 Sorted lib browser by sub count 2023-06-24 00:55:22 +02:00
Stefan Zermatten
5112ecd0c7 Fixed migration not counting collection subs 2023-06-24 00:50:34 +02:00
Stefan Zermatten
6c7308ebf8 Fixed library options not showing in create dialog 2023-06-24 00:28:03 +02:00
Stefan Zermatten
c50c512587 Fixed buff $target.var -> ~target.var
to skip crystallization
2023-06-23 23:31:54 +02:00
Stefan Zermatten
93dfbc8a93 Fixed search for library prop not using tags 2023-06-23 23:29:46 +02:00
Stefan Zermatten
2c89323764 Fixed errors with empty quantity ammo resources 2023-06-23 12:21:51 +02:00
Stefan Zermatten
33576e02fa Fixed users failing to create because of a bad hook 2023-06-23 09:55:48 +02:00
Stefan Zermatten
81cfc3919e Improved migrations 2023-06-23 09:42:14 +02:00
Stefan Zermatten
bae621cd47 Hotfix migrations 2023-06-22 15:52:42 +02:00
Stefan Zermatten
70edd7b2c0 Don't batch prop updates in migration to save memory
Should run slower, but within memory constraints
2023-06-22 13:45:55 +02:00
36 changed files with 552 additions and 138 deletions

View File

@@ -151,11 +151,6 @@ let CreatureSchema = new SimpleSchema({
blackbox: true,
defaultValue: {}
},
variables: {
type: Object,
blackbox: true,
defaultValue: {}
},
computeErrors: {
type: Array,
optional: true,

View File

@@ -225,7 +225,7 @@ function spendResources(prop, actionContext) {
throw 'The prop\'s ammo was not found on the creature';
}
if (
!itemConsumed.quantity.value ||
!itemConsumed?.quantity?.value ||
!isFinite(itemConsumed.quantity.value)
) return;
itemQuantityAdjustments.push({
@@ -247,8 +247,9 @@ function spendResources(prop, actionContext) {
} catch (e) {
actionContext.addLog({
name: 'Error',
value: e,
value: e.toString(),
});
console.error(e);
return true;
}
// No more errors should be thrown after this line

View File

@@ -100,7 +100,7 @@ function copyNodeListToTarget(propList, target, oldParent) {
/**
* Replaces all variables with their resolved values
* except variables of the form `$target.thing.total` become `thing.total`
* except variables of the form `~target.thing.total` become `thing.total`
*/
function crystalizeVariables({ propList, actionContext }) {
propList.forEach(prop => {
@@ -119,8 +119,8 @@ function crystalizeVariables({ propList, actionContext }) {
node.parseType !== 'accessor' && node.parseType !== 'symbol'
) return node;
// Handle variables
if (node.name === '$target') {
// strip $target
if (node.name === '~target') {
// strip ~target
if (node.parseType === 'accessor') {
node.name = node.path.shift();
if (!node.path.length) {
@@ -130,7 +130,7 @@ function crystalizeVariables({ propList, actionContext }) {
// Can't strip symbols
actionContext.addLog({
name: 'Error',
value: 'Variable `$target` should not be used without a property: $target.property',
value: 'Variable `~target` should not be used without a property: ~target.property',
});
}
return node;

View File

@@ -2,9 +2,8 @@ import operator from '/imports/parser/parseTree/operator.js';
import { parse } from '/imports/parser/parser.js';
import logErrors from './logErrors.js';
export default function applyEffectsToCalculationParseNode(calcObj, actionContext){
if (!calcObj.effects) return;
calcObj.effects.forEach(effect => {
export default function applyEffectsToCalculationParseNode(calcObj, actionContext) {
calcObj.effects?.forEach(effect => {
if (effect.operation !== 'add') return;
if (!effect.amount) return;
if (effect.amount.value === null) return;
@@ -17,8 +16,31 @@ export default function applyEffectsToCalculationParseNode(calcObj, actionContex
operator: '+',
fn: 'add'
});
} catch (e){
} catch (e) {
logErrors([e], actionContext)
}
});
// Add the highest proficiency as well
let highestProficiency;
calcObj.proficiencies?.forEach(proficiency => {
if (
proficiency.value > highestProficiency
|| (highestProficiency === undefined && Number.isFinite(proficiency.value))
) {
highestProficiency = proficiency.value;
}
});
if (highestProficiency) {
try {
let profParseNode = parse(highestProficiency.toString());
calcObj.parseNode = operator.create({
left: calcObj.parseNode,
right: profParseNode,
operator: '+',
fn: 'add'
});
} catch (e) {
logErrors([e], actionContext)
}
}
}

View File

@@ -2,7 +2,7 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, actionContext, context){
export default function recalculateCalculation(calc, actionContext, context) {
if (!calc?.parseNode) return;
calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, actionContext);

View File

@@ -29,9 +29,9 @@ export default function linkTypeDependencies(dependencyGraph, prop, computation)
function dependOnCalc({ dependencyGraph, prop, key }) {
let calc = get(prop, key);
if (!calc) return;
if (!calc?.type) return;
if (calc.type !== '_calculation') {
throw `Expected calculation got ${calc.type}`
throw `Failed to dependOnCal for prop: ${prop._id}, key: ${key}. Expected calculation got ${calc.type}`
}
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
}

View File

@@ -1,14 +1,14 @@
import resolve, { toString } from '/imports/parser/resolve.js';
export default function evaluateCalculation(calculation, scope, givenContext){
export default function evaluateCalculation(calculation, scope, givenContext) {
const parseNode = calculation.parseNode;
const fn = calculation._parseLevel;
const calculationScope = {...calculation._localScope, ...scope};
const {result: resultNode, context} = resolve(fn, parseNode, calculationScope, givenContext);
const calculationScope = { ...calculation._localScope, ...scope };
const { result: resultNode, context } = resolve(fn, parseNode, calculationScope, givenContext);
calculation.errors = context.errors;
if (resultNode?.parseType === 'constant'){
if (resultNode?.parseType === 'constant') {
calculation.value = resultNode.value;
} else if (resultNode?.parseType === 'error'){
} else if (resultNode?.parseType === 'error') {
calculation.value = null;
} else {
calculation.value = toString(resultNode);

View File

@@ -28,8 +28,8 @@ const duplicateLibraryNode = new ValidatedMethod({
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
numRequests: 4,
timeInterval: 6000,
},
run({ _id }) {
let libraryNode = LibraryNodes.findOne(_id);

View File

@@ -1,4 +1,4 @@
import { union, difference, sortBy, findLast } from 'lodash';
import { union, difference, sortBy, findLast, intersection } from 'lodash';
export function nodeArrayToTree(nodes) {
// Store a dict and list of all the nodes
@@ -83,9 +83,15 @@ export default function nodesToTree({
docs.forEach(doc => {
ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id));
});
// Remove the IDs of docs we have already found
// Get all the docs that are also ancestors and mark them
docs.forEach(doc => {
if (ancestorIds.includes(doc._id)) {
doc._ancestorOfMatchedDocument = true;
}
});
// Remove the ancestor IDs of docs we have already found
ancestorIds = difference(ancestorIds, docIds);
// Get the docs from the collection, don't worry about `removed` docs,
// Get the ancestor docs from the collection, don't worry about `removed` docs,
// if their descendant was not removed, neither are they
ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => {
// Mark that the nodes are ancestors of the found nodes

View File

@@ -252,7 +252,7 @@ Meteor.users.setPreference = new ValidatedMethod({
});
if (Meteor.isServer) {
Accounts.onCreateUser(() => {
Accounts.onCreateUser((options, user) => {
if (defaultLibraries?.length) {
Libraries.update({
_id: { $in: defaultLibraries }
@@ -271,6 +271,7 @@ if (Meteor.isServer) {
multi: true,
}, () => {/**/ });
}
return user;
});
}

View File

@@ -1,3 +1,3 @@
export default function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '');
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

View File

@@ -16,42 +16,27 @@ export default {
wideColumns: Boolean,
},
};
/*
Removed to improve chrome layout performance, put it back if there are rendering errors
.column-layout>span>div {
display: table;
table-layout: fixed;
}
*/
</script>
<style lang="css">
.column-layout {
column-count: 12;
column-fill: balance;
column-gap: 0;
column-gap: 8px;
column-width: 240px;
transform: translateZ(0);
padding: 4px;
padding: 8px;
}
.column-layout.wide-columns {
column-count: 12;
column-fill: balance;
column-gap: 0;
column-width: 320px;
transform: translateZ(0);
padding: 4px;
}
.column-layout>div,
.column-layout>*,
.column-layout>span>div {
width: 100%;
backface-visibility: hidden;
transform: translateX(0);
page-break-inside: avoid;
display: inline-block;
break-inside: avoid;
padding: 4px;
page-break-inside: avoid;
margin-bottom: 8px;
width: 100%;
}
</style>

View File

@@ -3,6 +3,19 @@
class="layout"
style="height: 100%;"
>
<div
v-if="$slots['left-tree']"
class="layout column justify-start"
:style="computedTreeStyle"
>
<slot
name="left-tree"
/>
</div>
<v-divider
v-if="$slots['left-tree']"
vertical
/>
<div
class="layout column justify-start"
:style="computedTreeStyle"

View File

@@ -90,6 +90,20 @@
<v-icon>mdi-content-duplicate</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item
v-if="$listeners && $listeners['make-reference']"
:disabled="context.editPermission === false"
@click="$emit('make-reference')"
>
<v-list-item-content>
<v-list-item-title>
Create Reference
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-link-plus</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item
v-if="$listeners && $listeners.move"
:disabled="context.editPermission === false"

View File

@@ -1,19 +1,111 @@
<template lang="html">
<v-combobox
v-model="filterTerms"
:items="filterOptions"
prepend-inner-icon="mdi-magnify"
hide-no-data
hide-selected
multiple
clearable
small-chips
deletable-chips
/>
<v-menu
v-model="menu"
:close-on-content-click="false"
>
<template #activator="{ on, attrs }">
<v-btn
v-bind="attrs"
icon
v-on="on"
>
<v-badge
:content="numFilters"
:value="numFilters"
color="primary"
overlap
>
<v-icon>mdi-magnify</v-icon>
</v-badge>
</v-btn>
</template>
<v-card>
<v-card-title>
Search
</v-card-title>
<v-card-text>
<v-select
v-model="typeFilterInput"
outlined
label="Type"
:items="filterOptions"
multiple
clearable
small-chips
deletable-chips
/>
<v-slide-x-transition group>
<div
v-for="(fieldFilter, index) in fieldFilters"
:key="index"
class="d-flex"
>
<v-text-field
v-model="fieldFilter.field"
class="text--mono"
label="Field"
outlined
/>
<v-text-field
v-model="fieldFilter.value"
label="Text"
class="ml-2"
outlined
/>
<v-btn
v-if="fieldFilters.length > 1"
icon
@click="fieldFilters.splice(index, 1)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
</v-slide-x-transition>
<div
v-if="fieldFilters.length < 5"
class="d-flex"
>
<v-spacer />
<v-btn
icon
@click="fieldFilters.push({name: '', value: undefined})"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
<v-card-actions>
<v-btn
text
@click="
fieldFilters = [{field: 'name', value: undefined}];
typeFilterInput = [];
menu = false;
"
>
<v-icon left>
mdi-close
</v-icon>
Clear
</v-btn>
<v-spacer />
<v-btn
text
color="primary"
@click="menu = false"
>
Find
</v-btn>
</v-card-actions>
</v-card-text>
</v-card>
</v-menu>
</template>
<script lang="js">
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import escapeRegex from '/imports/api/utility/escapeRegex.js';
const filterOptions = [];
for (let key in PROPERTIES) {
if (key === 'reference') continue;
@@ -31,39 +123,62 @@ export default {
},
},
data(){return {
filterTerms: [],
typeFilterInput: [],
fieldFilters: [{field: 'name', value: undefined}],
filterOptions,
menu: false,
}},
computed: {
filter(){
if (!this.filterTerms.length) return;
let typeFilters = [];
let nameFilters = [];
this.filterTerms.forEach(filter => {
if (filter.value){
typeFilters.push(filter.value);
filter() {
let filter = undefined;
if (this.typeFilterInput?.length) {
filter = filter || {};
filter.type = {$in: this.typeFilterInput};
}
this.fieldFilters?.forEach(fieldFilter => {
if (!fieldFilter.field || !fieldFilter.value) return;
const search = { $regex: escapeRegex(fieldFilter.value), '$options': 'i' };
filter = filter || {};
if (fieldFilter.field.includes('.')) {
// The user used dot notation, search exactly where they are looking
filter[fieldFilter.field] = search;
} else {
// escape string
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
var reg = new RegExp( '.*' + term + '.*', 'i' );
nameFilters.push(reg)
// No dot notation, search fields and their likely sub-fields
filter.$and = filter.$and || [];
filter.$and.push({
$or: [
{ [fieldFilter.field]: search },
{ [fieldFilter.field + '.calculation']: search },
{ [fieldFilter.field + '.text']: search },
],
});
}
});
let filter = {};
if (typeFilters.length){
filter.type = {$in: typeFilters};
}
if (nameFilters.length){
filter.name = {$in: nameFilters};
}
return filter;
},
},
watch:{
filter(value){
this.$emit('input', value);
extraFields() {
let extraFields = [];
this.fieldFilters?.forEach(fieldFilter => {
if (!fieldFilter.field || !fieldFilter.value) return;
extraFields.push(fieldFilter.field);
});
return extraFields;
},
numFilters() {
let numFilters = 0;
if (this.typeFilterInput?.length) numFilters += 1;
numFilters += this.extraFields.length;
return numFilters;
}
}
},
watch: {
menu(val) {
if (!val) {
this.$emit('input', this.filter);
this.$emit('extra-fields-changed', this.extraFields);
}
}
},
}
</script>

View File

@@ -11,9 +11,14 @@
<template slot="tree">
<v-toolbar
flat
dense
dark
style="flex-grow: 0;"
>
<tree-search-input
ref="searchBox"
v-model="filter"
class="mx-4"
/>
<v-spacer />
<v-switch
v-if="context.editPermission !== false"
@@ -23,12 +28,6 @@
:disabled="organizeDisabled"
style="flex-grow: 0; height: 32px;"
/>
<tree-search-input
ref="searchBox"
slot="extension"
v-model="filter"
class="mx-4"
/>
</v-toolbar>
<creature-properties-tree
class="pt-2 flex"

View File

@@ -596,7 +596,6 @@ export default {
margin-top: 4px;
margin-left: -30px;
padding-left: 34px;
z-index: -1;
}
.number-label .number {

View File

@@ -57,6 +57,7 @@ export default {
hideLibraryTab: true,
noBackdropClose: true,
showLibraryOnlyProps: true,
collection: 'libraryNodes',
},
callback(libraryNode){
if (!libraryNode) return;

View File

@@ -1,5 +1,12 @@
<template lang="html">
<tree-detail-layout>
<library-second-tree
v-if="showSecondTree"
slot="left-tree"
:selected-node="selectedNode"
@close="showSecondTree = false"
@selected="clickNode"
/>
<div
slot="tree"
class="layout column"
@@ -17,23 +24,44 @@
:dark="isToolbarDark"
:light="!isToolbarDark"
>
<tree-search-input
ref="searchBox"
v-model="filter"
class="mx-4"
@extra-fields-changed="val => extraFields = val"
/>
<v-spacer />
<v-fade-transition>
<v-menu v-if="organize && $vuetify.breakpoint.mdAndUp">
<template #activator="{ on, attrs }">
<v-btn
icon
v-bind="attrs"
v-on="on"
>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-card>
<v-card-text>
<v-switch
v-model="showSecondTree"
label="Show second library tree"
/>
</v-card-text>
</v-card>
</v-menu>
</v-fade-transition>
<v-switch
v-if="!libraryId || canEditLibrary"
v-model="organize"
hide-details
label="Organize"
class="mx-3"
class="ml-1 mr-3 mt-2"
style="flex-grow: 0; height: 32px;"
/>
<tree-search-input
ref="searchBox"
slot="extension"
v-model="filter"
class="mx-4"
/>
<insert-library-node-button
v-if="libraryId && canEditLibrary"
slot="extension"
style="bottom: -24px"
fab
:library-id="libraryId"
@@ -49,6 +77,7 @@
:library-id="libraryId"
:organize-mode="organize"
:selected-node="selectedNode"
:extra-fields="extraFields"
should-subscribe
:filter="filter"
@selected="clickNode"
@@ -93,6 +122,7 @@ import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
import TreeSearchInput from '/imports/client/ui/components/tree/TreeSearchInput.vue';
import LibrarySecondTree from '/imports/client/ui/library/LibrarySecondTree.vue';
export default {
components: {
@@ -102,6 +132,7 @@ export default {
LibraryContentsContainer,
InsertLibraryNodeButton,
TreeSearchInput,
LibrarySecondTree,
},
props: {
selection: Boolean,
@@ -114,6 +145,8 @@ export default {
organize: false,
selectedNodeId: undefined,
filter: undefined,
extraFields: [],
showSecondTree: false,
};},
computed: {
isToolbarDark(){
@@ -121,7 +154,7 @@ export default {
this.selectedNode && this.selectedNode.color ||
getThemeColor('secondary')
);
}
},
},
watch:{
selectedNode(val){

View File

@@ -5,7 +5,7 @@
:class="isSelected && !disabled && 'primary--text v-list-item--active'"
>
<v-list-item-action
v-if="selection"
v-if="selection && !singleSelect"
>
<v-checkbox
:disabled="disabled"
@@ -66,6 +66,7 @@ export default {
},
open: Boolean,
selection: Boolean,
singleSelect: Boolean,
dense: Boolean,
isSelected: Boolean,
disabled: Boolean,

View File

@@ -50,6 +50,10 @@ export default {
type: Object,
default: undefined,
},
extraFields: {
type: Array,
default: undefined,
},
},
data() {
return {
@@ -75,7 +79,7 @@ export default {
$subscribe: {
'libraryNodes'() {
if (this.slowShouldSubscribe) {
return [this.libraryId];
return [this.libraryId, this.extraFields];
} else {
return [];
}

View File

@@ -9,6 +9,7 @@
:model="library"
:to="{ name: 'singleLibrary', params: { id: library._id }}"
:selection="selection"
:single-select="singleSelect"
:is-selected="librariesSelected && librariesSelected.includes(library._id)"
:selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)"
:disabled="disabled"
@@ -26,6 +27,7 @@
:open="openCollections[libraryCollection._id]"
:model="libraryCollection"
:selection="selection"
:single-select="singleSelect"
:is-selected="libraryCollectionsSelected && libraryCollectionsSelected.includes(libraryCollection._id)"
:disabled="disabled"
@select="val => $emit('select-library-collection', libraryCollection._id, val)"
@@ -37,6 +39,7 @@
:model="library"
:to="{ name: 'singleLibrary', params: { id: library._id }}"
:selection="selection"
:single-select="singleSelect"
:is-selected="librariesSelected && librariesSelected.includes(library._id)"
:selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)"
:disabled="disabled"
@@ -44,6 +47,14 @@
@select="val => $emit('select-library', library._id, val)"
/>
</v-list-group>
<v-list-item v-if="!$subReady.libraries">
<v-spacer />
<v-progress-circular
indeterminate
color="primary"
/>
<v-spacer />
</v-list-item>
</v-list>
</template>
@@ -62,6 +73,7 @@ export default {
},
props: {
selection: Boolean,
singleSelect: Boolean,
disabled: Boolean,
librariesSelected: {
type: Array,
@@ -87,7 +99,7 @@ export default {
libraryCollections(){
const userId = Meteor.userId();
if (!userId) return;
const subCollections = Meteor.user().subscribedLibraryCollections || [];
const subCollections = Meteor.user()?.subscribedLibraryCollections || [];
return LibraryCollections.find({
$or: [
{ owner: userId },

View File

@@ -6,9 +6,10 @@
v-bind="$attrs"
:class="(isSelected || selectedByCollection) && !disabled && 'primary--text v-list-item--active'"
:to="selection ? undefined : to"
@click="singleSelect && $emit('select')"
>
<v-list-item-action
v-if="selection"
v-if="selection && !singleSelect"
>
<v-checkbox
:disabled="disabled"
@@ -42,6 +43,7 @@ export default {
required: true,
},
selection: Boolean,
singleSelect: Boolean,
isSelected: Boolean,
selectedByCollection: Boolean,
disabled: Boolean,

View File

@@ -10,6 +10,7 @@
@move="move"
@copy="copy"
@remove="remove"
@make-reference="makeReference"
@toggle-editing="editing = !editing"
@color-changed="value => change({path: ['color'], value})"
/>
@@ -206,6 +207,26 @@ export default {
}
});
},
makeReference() {
insertNode.call({
libraryNode: {
type: 'reference',
ref: {
collection: 'libraryNodes',
id: this.model._id,
},
order: (this.model.order || 0) + 0.5,
},
parentRef: this.model.parent,
}, (error, docId) => {
if (error) console.error(error);
if (this.embedded){
this.$emit('duplicated', docId);
} else {
this.$store.dispatch('popDialogStack');
}
});
},
selectSubProperty(_id) {
if (this.embedded) {
this.$emit('select-sub-property', _id);
@@ -324,6 +345,7 @@ export default {
suggestedType,
noBackdropClose: true,
showLibraryOnlyProps: true,
collection: 'libraryNodes',
},
callback(result){
if (!result) return;

View File

@@ -0,0 +1,125 @@
<template lang="html">
<div class="d-flex flex-column fill-height">
<v-fade-transition mode="out-in">
<v-toolbar
v-if="libraryId"
dark
flat
color="secondary"
>
<v-btn
icon
@click="libraryId = undefined"
>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title
key="library-name"
class="d-flex"
>
<div class="flex-shrink-1">
{{ library && library.name }}
</div>
<v-spacer />
</v-toolbar-title>
<v-btn
v-if="library && ($route.params.id !== library._id)"
icon
@click="libraryId = undefined; $router.push({ name: 'singleLibrary', params: { id: library._id }})"
>
<v-icon>mdi-arrow-right-bold</v-icon>
</v-btn>
</v-toolbar>
<v-toolbar
v-else
dark
flat
color="secondary"
>
<v-toolbar-title
key="no-library"
>
<v-btn
icon
@click="$emit('close')"
>
<v-icon>mdi-close</v-icon>
</v-btn>
Select Library
</v-toolbar-title>
</v-toolbar>
</v-fade-transition>
<v-sheet
class="pa-3 flex-grow-1 flex-shrink-1"
style="overflow: auto;"
>
<v-fade-transition mode="out-in">
<library-list
v-if="!libraryId"
selection
single-select
@select-library="id => libraryId = id"
/>
<library-contents-container
v-else
:library-id="libraryId"
:organize-mode="canEditLibrary"
should-subscribe
:selected-node="selectedNode"
@selected="e => $emit('selected', e)"
/>
</v-fade-transition>
</v-sheet>
</div>
</template>
<script lang="js">
import LibraryList from '/imports/client/ui/library/LibraryList.vue';
import LibraryContentsContainer from '/imports/client/ui/library/LibraryContentsContainer.vue';
import Libraries from '/imports/api/library/Libraries.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
export default {
components: {
LibraryList,
LibraryContentsContainer,
},
props: {
selectedNode: {
type: Object,
default: undefined,
},
},
data() {
return {
libraryId: undefined
};
},
meteor: {
$subscribe: {
'library'(){
if (this.libraryId){
return [this.libraryId]
} else {
return [];
}
},
},
library() {
return Libraries.findOne(this.libraryId);
},
canEditLibrary(){
if (!this.libraryId) return;
try {
assertEditPermission(this.library, Meteor.userId());
return true;
} catch (e){
return false;
}
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -75,7 +75,7 @@
</template>
<script lang="js">
import { sortBy } from 'lodash';
import { orderBy } from 'lodash';
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import Libraries from '/imports/api/library/Libraries.js';
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
@@ -122,7 +122,7 @@ export default {
});
},
libraryCards() {
return sortBy([...this.libraries, ...this.collections], 'name');
return orderBy([...this.libraries, ...this.collections], ['subscriberCount', 'name'], ['desc', 'asc']);
},
},
methods: {

View File

@@ -80,6 +80,7 @@
no-child-insert
:model="model"
:errors="errors"
:collection="collection"
@change="change"
@push="push"
@pull="pull"
@@ -229,6 +230,10 @@ export default {
type: Array,
default: undefined,
},
collection: {
type: String,
default: undefined,
},
suggestedType: {
type: String,
default: undefined,
@@ -250,7 +255,7 @@ export default {
},
reactiveProvide: {
name: 'context',
include: ['debounceTime'],
include: ['debounceTime', 'isLibraryForm'],
},
data(){return {
selectedNodeIds: [],
@@ -274,6 +279,9 @@ export default {
const propDef = PROPERTIES[this.type];
return propDef && propDef.docsPath;
},
isLibraryForm() {
return this.collection === 'libraryNodes' || undefined;
},
},
watch: {
type(newType){

View File

@@ -30,7 +30,7 @@
v-else-if="model.attributeType === 'resource'"
:model="model"
@click="$emit('click')"
@change="({ type, value }) => damageProperty({type, value: -value})"
@change="({ type, value, ack }) => damageProperty({type, value: -value, ack})"
@mouseover="hover = true"
@mouseleave="hover = false"
/>
@@ -96,6 +96,9 @@ export default {
_id: this.model._id,
operation: change.type,
value: change.value
}, e => {
console.log(change);
change.ack?.(e);
});
},
log({_id}) {

View File

@@ -1,3 +0,0 @@
RegExp.escape = function(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
};

View File

@@ -2,7 +2,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
import { get } from 'lodash';
const dollarSignRegex = /(\W)\$(\w+)/gi;
const dollarSignRegex = /(\W|^)\$(\w+)/gi;
export default function migrate1To2(archive) {
archive.properties = archive.properties.map(prop => {
@@ -10,6 +10,10 @@ export default function migrate1To2(archive) {
// Migrate slot fillers to folders
if (prop.type === 'slotFiller') {
prop.type = 'folder';
// If the slot filler has a description, change it to a computed one
if (typeof prop.description == 'string') {
prop.description = { text: prop.description };
}
}
// Migrate slot filler slot type to folders
if (prop.slotType === 'slotFiller') {

View File

@@ -32,12 +32,15 @@ Migrations.add({
});
function migrateCollection(collection, migrateDoc) {
const bulk = collection.rawCollection().initializeUnorderedBulkOp();
collection.find({}).forEach(doc => migrateDoc(bulk, doc, collection));
bulk.execute();
collection.find({}).forEach((doc, index) => {
if (index % 1000 === 0) {
console.log(`Migrating document #${index}`);
}
migrateDoc(doc, collection)
});
}
export function migratePropUp(bulk, prop, collection) {
export function migratePropUp(prop, collection) {
let update;
if (prop.type === 'slotFiller') {
update = update || { $set: {} };
@@ -48,6 +51,10 @@ export function migratePropUp(bulk, prop, collection) {
update.$set.slotFillImage = prop.picture;
update.$unset = { picture: 1 };
}
// If the slot filler has a description, change it to a computed one
if (typeof prop.description == 'string') {
prop.description = { text: prop.description };
}
}
// Don't look for slot fillers
@@ -67,13 +74,19 @@ export function migratePropUp(bulk, prop, collection) {
// Replace dollar sign with tilde in calculated fields
update = dollarSignToTilde(prop, update);
// Add the update to the bulk op
// update the document
if (update) {
bulk.find({ _id: prop._id }).updateOne(update);
try {
collection.update({ _id: prop._id }, update, { bypassCollection2: true }, e => {
if (e) console.warn('Doc Migration failed: ', prop._id, e);
});
} catch (e) {
console.warn('Doc Migration failed: ', prop._id, e);
}
}
}
export function migratePropDown(bulk, prop) {
export function migratePropDown(prop, collection) {
const update = {
$unset: {
slotFillImage: 1,
@@ -88,7 +101,15 @@ export function migratePropDown(bulk, prop) {
tags: union(prop.libraryTags, prop.tags)
}
}
bulk.find({ _id: prop._id }).updateOne(update);
if (update) {
try {
collection.update({ _id: prop._id }, update, { bypassCollection2: true }, e => {
if (e) console.warn('Doc Migration failed: ', prop._id, e);
});
} catch (e) {
console.warn('Doc Migration failed: ', prop._id, e);
}
}
}
function countSubscribers() {
@@ -104,7 +125,7 @@ function countSubscribers() {
});
bulkLib.execute();
const bulkLibCols = Libraries.rawCollection().initializeUnorderedBulkOp();
const bulkLibCols = LibraryCollections.rawCollection().initializeUnorderedBulkOp();
LibraryCollections.find({}, {
fields: { _id: 1 }
}).forEach(col => {
@@ -117,7 +138,7 @@ function countSubscribers() {
bulkLibCols.execute();
}
const dollarSignRegex = /(\W)\$(\w+)/gi;
const dollarSignRegex = /(\W|^)\$(\w+)/gi;
function dollarSignToTilde(prop, update) {
computedSchemas[prop.type]?.inlineCalculationFields()?.forEach(calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => {

View File

@@ -203,22 +203,42 @@ let libraryIdSchema = new SimpleSchema({
},
});
Meteor.publish('libraryNodes', function (libraryId) {
const extraFieldsSchema = new SimpleSchema({
extraFields: {
type: Array,
optional: true,
},
'extraFields.$': {
type: String,
},
});
Meteor.publish('libraryNodes', function (libraryId, extraFields) {
if (!libraryId) return [];
libraryIdSchema.validate({ libraryId });
try {
libraryIdSchema.validate({ libraryId });
extraFieldsSchema.validate({ extraFields });
} catch (e) {
return this.error(e);
}
this.autorun(function () {
let userId = this.userId;
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch (e) {
try {
assertViewPermission(library, userId)
} catch (e) {
return this.error(e);
}
const fields = { ...LIBRARY_NODE_TREE_FIELDS };
extraFields?.forEach(field => {
fields[field] = 1;
});
return [
LibraryNodes.find({
'ancestors.id': libraryId,
}, {
sort: { order: 1 },
fields: LIBRARY_NODE_TREE_FIELDS,
fields,
}),
];
});

View File

@@ -86,7 +86,13 @@ Meteor.publish('searchLibraryNodes', function (creatureId) {
let options = undefined;
if (searchTerm) {
filter.name = { $regex: escapeRegex(searchTerm), '$options': 'i' };
// Regex search instead of text index
filter.$and = [{
$or: [
{ name: { $regex: escapeRegex(searchTerm), '$options': 'i' } },
{ libraryTags: searchTerm },
],
}];
// filter.$text = {$search: searchTerm};
options = {
/*
@@ -105,7 +111,7 @@ Meteor.publish('searchLibraryNodes', function (creatureId) {
}
} else {
//delete filter.$text
delete filter.name;
delete filter.$and;
options = {
sort: {
'ancestors.0.id': 1,

View File

@@ -41,16 +41,21 @@ Meteor.publish('slotFillers', function (slotId, searchTerm, isDummySlot) {
sort: { name: 1 }
});
// Build a filter for nodes in those libraries that match the slot
let filter = getSlotFillFilter({ slot, libraryIds });
this.autorun(function () {
// Build a filter for nodes in those libraries that match the slot
let filter = getSlotFillFilter({ slot, libraryIds });
// Get the limit of the documents the user can fetch
var limit = self.data('limit') || 50;
check(limit, Number);
let options = undefined;
if (searchTerm) {
filter.name = { $regex: escapeRegex(searchTerm), '$options': 'i' };
filter.$and.push({
$or: [
{ name: { $regex: escapeRegex(searchTerm), '$options': 'i' } },
{ libraryTags: searchTerm }
]
});
//filter.$text = { $search: searchTerm };
options = {
// relevant documents have a higher score.

View File

@@ -1,6 +1,6 @@
{
"name": "dicecloud",
"version": "2.0.52",
"version": "2.0.54",
"description": "Unofficial Online Realtime D&D 5e App",
"license": "GPL-3.0",
"repository": {

View File

@@ -153,7 +153,7 @@
"order": 8,
"urlName": "buff",
"href": "/docs/property/buff",
"description": "Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. \n\nBuffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property.\n\n### Variable freezing\n\nWhen a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the `don't freeze variables` option, or on an individual variable reference by prefixing the variable with the keyword `$target.`.\n\nFor example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `$target.strength + dexterity` the property's calculation will become `strength + 16` when it is copied to the target character.\n\n---\n\n### Name\n\nThe name of the buff.\n\n### Description\n\nDescription of the applied buff.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Target\n\n- **Target** Apply the buff to the target of the action\n- **Self** Apply the buff to the creature taking the action\n\n### Hide remove button\n\nIf this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action.\n\n### Don't show in log\n\nIf set, the buff will not show its name and description in the log when applied.\n\n### Don't freeze variables\n\nPrevent the buff from freezing variables in child property calculations to their value at the time the buff was applied.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)",
"description": "Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. \n\nBuffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property.\n\n### Variable freezing\n\nWhen a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the `don't freeze variables` option, or on an individual variable reference by prefixing the variable with the keyword `~target.`.\n\nFor example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `~target.strength + dexterity` the property's calculation will become `strength + 16` when it is copied to the target character.\n\n---\n\n### Name\n\nThe name of the buff.\n\n### Description\n\nDescription of the applied buff.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Target\n\n- **Target** Apply the buff to the target of the action\n- **Self** Apply the buff to the creature taking the action\n\n### Hide remove button\n\nIf this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action.\n\n### Don't show in log\n\nIf set, the buff will not show its name and description in the log when applied.\n\n### Don't freeze variables\n\nPrevent the buff from freezing variables in child property calculations to their value at the time the buff was applied.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)",
"published": true
},
{