More of the sheet conforms to library allowances

This commit is contained in:
Stefan Zermatten
2022-07-20 00:09:58 +02:00
parent 0c06f60b7e
commit 6f7e742eb9
10 changed files with 377 additions and 173 deletions

View File

@@ -0,0 +1,90 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
import SimpleSchema from 'simpl-schema';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
const changeAllowedLibraries = new ValidatedMethod({
name: 'creatures.changeAllowedLibraries',
mixins: [RateLimiterMixin, simpleSchemaMixin],
schema: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
allowedLibraries: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
allowedLibraryCollections: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}),
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({_id, allowedLibraries, allowedLibraryCollections}) {
let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId);
let $set;
if (allowedLibraries) {
$set = { allowedLibraries }
}
if (allowedLibraryCollections) {
if (!$set) $set = {};
$set.allowedLibraryCollections = allowedLibraryCollections;
}
if (!$set) return;
Creatures.update(_id, {$set});
},
});
const toggleAllUserLibraries = new ValidatedMethod({
name: 'creatures.removeLibraryLimits',
mixins: [RateLimiterMixin, simpleSchemaMixin],
schema: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
value: {
type: Boolean,
},
}),
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({_id, value}) {
if (value) {
Creatures.update(_id, {
$unset: {
allowedLibraryCollections: 1,
allowedLibraries: 1,
},
});
} else {
Creatures.update(_id, {
$set: {
allowedLibraryCollections: [],
allowedLibraries: [],
},
});
}
},
});
export {changeAllowedLibraries, toggleAllUserLibraries};

View File

@@ -2,3 +2,4 @@ import '/imports/api/creature/creatures/methods/insertCreature.js';
import '/imports/api/creature/creatures/methods/removeCreature.js';
import '/imports/api/creature/creatures/methods/restCreature.js';
import '/imports/api/creature/creatures/methods/updateCreature.js';
import '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';

View File

@@ -7,7 +7,7 @@ import defaultCharacterProperties from '/imports/api/creature/creatures/defaultC
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js';
import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js';
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences.js';
import SimpleSchema from 'simpl-schema';
@@ -78,7 +78,7 @@ const insertCreature = new ValidatedMethod({
// If the user only has a single ruleset subscribed, use it by default
if (Meteor.isServer) {
insertDefaultRuleset(baseId, userId, rulesetSlot);
insertDefaultRuleset(creatureId, baseId, userId, rulesetSlot);
}
return creatureId;
@@ -86,8 +86,8 @@ const insertCreature = new ValidatedMethod({
});
// If the user only has a single ruleset subscribed, insert it by default
function insertDefaultRuleset(baseId, userId, slot) {
const libraryIds = getUserLibraryIds(userId);
function insertDefaultRuleset(creatureId, baseId, userId, slot) {
const libraryIds = getCreatureLibraryIds(creatureId, userId);
const filter = getSlotFillFilter({ slot, libraryIds });
const fillCursor = LibraryNodes.find(filter, { fields: { _id: 1 } });
const numRulesets = fillCursor.count();

View File

@@ -1,6 +1,8 @@
import { check } from 'meteor/check';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js';
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){
@@ -37,7 +39,7 @@ Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){
})];
});
Meteor.publish('searchLibraryNodes', function(){
Meteor.publish('searchLibraryNodes', function(creatureId){
let self = this;
this.autorun(function (){
let type = self.data('type');
@@ -49,23 +51,12 @@ Meteor.publish('searchLibraryNodes', function(){
}
// Get all the ids of libraries the user can access
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
});
if (!user) return [];
const subs = user.subscribedLibraries || [];
let libraries = Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{_id: {$in: subs}},
]
}, {
fields: {_id: 1, name: 1},
});
let libraryIds = libraries.map(lib => lib._id);
let libraryIds;
if (creatureId) {
libraryIds = getCreatureLibraryIds(creatureId, userId)
} else {
libraryIds = getUserLibraryIds(userId)
}
// Build a filter for nodes in those libraries that match the type
let filter = {
@@ -122,6 +113,7 @@ Meteor.publish('searchLibraryNodes', function(){
});
let cursor = LibraryNodes.find(filter, options);
const libraries = Libraries.find({ _id: { $in: libraryIds } });
Mongo.Collection._publishCursor(libraries, self, 'libraries');

View File

@@ -3,7 +3,7 @@ import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js';
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
import { LIBRARY_NODE_TREE_FIELDS } from '/imports/server/publications/library.js';
const FIELDS = {
@@ -28,7 +28,8 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
}
// Get all the ids of libraries the user can access
const libraryIds = getUserLibraryIds(userId);
const creatureId = slot.ancestors[0].id;
const libraryIds = getCreatureLibraryIds(creatureId, userId);
const libraries = Libraries.find({
$or: [
{ owner: userId },

View File

@@ -33,7 +33,7 @@
@change="(value, ack) => $emit('change', {path: ['avatarPicture'], value, ack})"
/>
<form-sections>
<form-section name="settings">
<form-section name="Settings">
<v-switch
label="Hide redundant stats"
:input-value="model.settings.hideUnusedStats"
@@ -88,17 +88,48 @@
/>
-->
</form-section>
<form-section name="Libraries">
<smart-switch
label="All user libraries"
@change="allUserLibrariesChange"
/>
<library-list
selection
:disabled="!model.allowedLibraries && !model.allowedLibraryCollections"
:libraries-selected="model.allowedLibraries"
:library-collections-selected="model.allowedLibraryCollections"
:libraries-selected-by-collections="librariesSelectedByCollections"
@select-library="selectLibrary"
@select-library-collection="selectLibraryCollection"
/>
<v-progress-linear
v-if="libraryWriteLoading"
style="margin: 12px -24px -16px -24px; width: calc(100% + 48px);"
indeterminate
/>
<p
v-if="libraryWriteError"
class="text--error"
>
{{ libraryWriteError }}
</p>
</form-section>
</form-sections>
</div>
</template>
<script lang="js">
import { union, without, debounce } from 'lodash';
import FormSection, {FormSections} from '/imports/ui/properties/forms/shared/FormSection.vue';
import LibraryList from '/imports/ui/library/LibraryList.vue';
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import {changeAllowedLibraries, toggleAllUserLibraries} from '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
export default {
components: {
FormSection,
FormSections,
FormSections,
LibraryList,
},
props: {
stored: {
@@ -116,7 +147,64 @@ export default {
type: Boolean,
},
disabled: Boolean,
},
},
data() { return {
libraryCollections: this.model.allowedLibraryCollections,
libraries: this.model.allowedLibraries,
libraryWriteLoading: false,
libraryWriteError: undefined,
dirty: false, // If there are pending changes
}
},
watch: {
'model.allowedLibraryCollections': function (newVal) {
if (!this.dirty) this.libraryCollections = newVal;
},
'model.allowedLibraries': function (newVal) {
if (!this.dirty) this.libraries = newVal;
},
},
mounted() {
this.updateAllowedLibraryCollections = debounce(() => {
this.libraryWriteLoading = true;
this.dirty = false;
changeAllowedLibraries.call({
_id: this.model._id,
allowedLibraryCollections: this.libraryCollections,
}, error => {
this.libraryWriteLoading = false;
this.libraryWriteError = error;
});
}, 500);
this.updateAllowedLibraries = debounce(() => {
this.libraryWriteLoading = true;
this.dirty = false;
changeAllowedLibraries.call({
_id: this.model._id,
allowedLibraries: this.libraries,
}, error => {
this.libraryWriteLoading = false;
this.libraryWriteError = error;
});
}, 500);
},
meteor: {
$subscribe: {
'libraries': [],
},
librariesSelectedByCollections() {
let ids = [];
LibraryCollections.find({
_id: { $in: this.model.allowedLibraryCollections }
}).forEach(collection => {
ids = union(ids, collection.libraries);
});
return ids;
},
},
methods: {
changeShowTreeTab(value){
this.$emit('change', {
@@ -144,6 +232,30 @@ export default {
);
}
},
allUserLibrariesChange(val, ack) {
toggleAllUserLibraries.call({
_id: this.model._id,
val
}, error => ack(error));
},
selectLibrary(id, val) {
if (val) {
this.libraries = union(this.libraries, [id]);
} else {
this.libraries = without(this.libraries, id);
}
this.dirty = true;
this.updateAllowedLibraries();
},
selectLibraryCollection(id, val) {
if (val) {
this.libraryCollections = union(this.libraryCollections, [id]);
} else {
this.libraryCollections = without(this.libraryCollections, id);
}
this.dirty = true;
this.updateAllowedLibraryCollections();
},
},
};
</script>

View File

@@ -195,7 +195,7 @@ export default {
} else {
this.$store.commit(
'setTabForCharacterSheet',
{id: creatureId, tab: 4}
{id: creatureId, tab: 5}
);
this.$emit('pop', creatureId);
defer(() => {

View File

@@ -170,6 +170,7 @@
data: {
parentDoc: forcedType ? undefined : parent,
forcedType,
creatureId: this.creatureId,
},
callback(result){
if (!result){

View File

@@ -171,160 +171,166 @@
</template>
<script lang="js">
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
import Libraries from '/imports/api/library/Libraries.js';
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import PropertySelector from '/imports/ui/properties/shared/PropertySelector.vue';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
import Libraries from '/imports/api/library/Libraries.js';
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import PropertySelector from '/imports/ui/properties/shared/PropertySelector.vue';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
PropertySelector,
DialogBase,
TreeNodeView,
LibraryNodeExpansionContent,
...propertyFormIndex,
export default {
components: {
PropertySelector,
DialogBase,
TreeNodeView,
LibraryNodeExpansionContent,
...propertyFormIndex,
},
mixins: [schemaFormMixin],
props: {
creatureId: {
type: String,
default: undefined,
},
mixins: [schemaFormMixin],
props: {
forcedType: {
type: String,
default: undefined,
},
suggestedTypes: {
type: Array,
default: undefined,
},
suggestedType: {
type: String,
default: undefined,
},
parentDoc: {
type: Object,
default: undefined,
},
forcedType: {
type: String,
default: undefined,
},
reactiveProvide: {
name: 'context',
include: ['debounceTime'],
suggestedTypes: {
type: Array,
default: undefined,
},
data(){return {
selectedNodeIds: [],
type: this.forcedType || this.suggestedType,
model: {
type: this.type,
},
searchValue: undefined,
debounceTime: 0,
tab: 0,
};},
computed: {
typeName(){
return getPropertyName(this.type) || 'Property';
},
toolbarColor(){
return getThemeColor('secondary');
}
suggestedType: {
type: String,
default: undefined,
},
watch: {
type(newType){
this.changeType(newType);
},
parentDoc: {
type: Object,
default: undefined,
},
mounted(){
this.changeType(this.type);
},
reactiveProvide: {
name: 'context',
include: ['debounceTime'],
},
data(){return {
selectedNodeIds: [],
type: this.forcedType || this.suggestedType,
model: {
type: this.type,
},
methods: {
propertyHelpChanged(value){
Meteor.users.setPreference.call({
preference: 'hidePropertySelectDialogHelp',
value: !value
}, error => {
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
},
searchChanged(val, ack){
this._subs.searchLibraryNodes.setData('searchTerm', val);
this._subs.searchLibraryNodes.setData('limit', undefined);
this.selectedNode = undefined;
this.searchValue = val;
setTimeout(ack, 200);
},
loadMore(){
if (this.currentLimit >= this.countAll) return;
this._subs.searchLibraryNodes.setData('limit', this.currentLimit + 32);
},
insert(){
if (!this.selectedNodeIds.length) return;
this.$store.dispatch('popDialogStack', this.selectedNodeIds);
},
changeType(type){
this._subs.searchLibraryNodes.setData('type', type);
if (!type) return;
this.tab = 1;
this.schema = propertySchemasIndex[type];
this.validationContext = this.schema.newContext();
let model = this.schema.clean({});
model.type = type;
this.model = model;
},
openPropertyDetails(id){
this.$store.commit('pushDialogStack', {
component: 'library-node-dialog',
elementId: id,
data: {
_id: id,
},
});
},
searchValue: undefined,
debounceTime: 0,
tab: 0,
};},
computed: {
typeName(){
return getPropertyName(this.type) || 'Property';
},
meteor: {
'$subscribe':{
'searchLibraryNodes': [],
'selectedLibraryNodes'(){
return [this.selectedNodeIds];
},
},
showPropertyHelp(){
let user = Meteor.user();
return !(user?.preferences?.hidePropertySelectDialogHelp)
},
currentLimit(){
return this._subs.searchLibraryNodes.data('limit') || 32;
},
countAll(){
return this._subs.searchLibraryNodes.data('countAll');
},
libraryNodes(){
return LibraryNodes.find({
_searchResult: true
},{
sort: {
'ancestors.0.id': 1,
name: 1,
order: 1,
},
});
},
libraryNames(){
let names = {};
Libraries.find().forEach(lib => names[lib._id] = lib.name)
return names;
}
toolbarColor(){
return getThemeColor('secondary');
}
};
},
watch: {
type(newType){
this.changeType(newType);
},
},
mounted(){
this.changeType(this.type);
},
methods: {
propertyHelpChanged(value){
Meteor.users.setPreference.call({
preference: 'hidePropertySelectDialogHelp',
value: !value
}, error => {
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
},
searchChanged(val, ack){
this._subs.searchLibraryNodes.setData('searchTerm', val);
this._subs.searchLibraryNodes.setData('limit', undefined);
this.selectedNode = undefined;
this.searchValue = val;
setTimeout(ack, 200);
},
loadMore(){
if (this.currentLimit >= this.countAll) return;
this._subs.searchLibraryNodes.setData('limit', this.currentLimit + 32);
},
insert(){
if (!this.selectedNodeIds.length) return;
this.$store.dispatch('popDialogStack', this.selectedNodeIds);
},
changeType(type){
this._subs.searchLibraryNodes.setData('type', type);
if (!type) return;
this.tab = 1;
this.schema = propertySchemasIndex[type];
this.validationContext = this.schema.newContext();
let model = this.schema.clean({});
model.type = type;
this.model = model;
},
openPropertyDetails(id){
this.$store.commit('pushDialogStack', {
component: 'library-node-dialog',
elementId: id,
data: {
_id: id,
},
});
},
},
meteor: {
'$subscribe':{
'searchLibraryNodes'() {
return [this.creatureId]
},
'selectedLibraryNodes'(){
return [this.selectedNodeIds];
},
},
showPropertyHelp(){
let user = Meteor.user();
return !(user?.preferences?.hidePropertySelectDialogHelp)
},
currentLimit(){
return this._subs.searchLibraryNodes.data('limit') || 32;
},
countAll(){
return this._subs.searchLibraryNodes.data('countAll');
},
libraryNodes(){
return LibraryNodes.find({
_searchResult: true
},{
sort: {
'ancestors.0.id': 1,
name: 1,
order: 1,
},
});
},
libraryNames(){
let names = {};
Libraries.find().forEach(lib => names[lib._id] = lib.name)
return names;
}
}
};
</script>
<style lang="css" scoped>

View File

@@ -276,6 +276,7 @@ export default {
elementId: 'insert-creature-property-btn',
data: {
parentDoc: this.model,
creatureId: this.creatureId,
},
callback(result){
if (!result) return;