Compare commits

...

5 Commits
1.3.2 ... 1.3.4

Author SHA1 Message Date
Stefan Zermatten
2a332a2965 Merge branch 'feature-parties' 2017-07-14 17:09:47 +02:00
Stefan Zermatten
44a1daf6f8 Grouping characters by party now works
closes #75, finally
2017-07-14 17:09:30 +02:00
Stefan Zermatten
ac23afac5d Stopped characters with poor names from having failed URL's 2017-07-13 17:16:23 +02:00
Stefan Zermatten
a411fb2b43 Fixed migration for failed slug strings 2017-07-13 16:44:23 +02:00
Stefan Zermatten
35b6fe20ae Reset migrations to line up with current DiceCloud server 2017-07-13 16:16:00 +02:00
16 changed files with 368 additions and 238 deletions

View File

@@ -1,8 +1,42 @@
Parties = new Mongo.Collection("parties");
Schemas.Party = new SimpleSchema({
//each character/monster can only be in one party at a time
//each party can only be in a single instance at a time
name: {
type: String,
defaultValue: "New Party",
trim: false,
optional: true,
},
characters: {
type: [String],
regEx: SimpleSchema.RegEx.Id,
index: 1,
defaultValue: [],
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Parties.attachSchema(Schemas.Party);
Parties.allow({
insert: function(userId, doc) {
return userId && doc.owner === userId;
},
update: function(userId, doc, fields, modifier) {
return userId && doc.owner === userId;
},
remove: function(userId, doc) {
return userId && doc.owner === userId;
},
fetch: ["owner"],
});
Parties.deny({
update: function(userId, docs, fields, modifier) {
// can't change owners
return _.contains(fields, "owner");
}
});

View File

@@ -4,7 +4,7 @@ Characters = new Mongo.Collection("characters");
Schemas.Character = new SimpleSchema({
//strings
name: {type: String, defaultValue: "", trim: false, optional: true},
urlName: {type: String, defaultValue: "", trim: false, optional: true},
urlName: {type: String, defaultValue: "-", trim: false, optional: true},
alignment: {type: String, defaultValue: "", trim: false, optional: true},
gender: {type: String, defaultValue: "", trim: false, optional: true},
race: {type: String, defaultValue: "", trim: false, optional: true},
@@ -543,10 +543,13 @@ if (Meteor.isServer){
});
Characters.after.update(function(userId, doc, fieldNames, modifier, options) {
if (_.contains(fieldNames, "name")){
var urlName = getSlug(doc.name, {maintainCase: true});
var urlName = getSlug(doc.name, {maintainCase: true}) || "-";
Characters.update(doc._id, {$set: {urlName}});
}
});
Characters.before.insert(function(userId, doc) {
doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-";
});
}
Characters.allow({

View File

@@ -43,7 +43,7 @@ Router.map(function() {
var _id = this.params._id
var character = Characters.findOne(_id);
var urlName = character && character.urlName;
var path = `\/character\/${_id}\/${urlName}`;
var path = `\/character\/${_id}\/${urlName || "-"}`;
Router.go(path,{},{replaceState:true});
},
});

View File

@@ -0,0 +1,3 @@
Template.registerHelper("characterPath", function(char) {
return `\/character\/${char._id}\/${char.urlName || "-"}`;
});

View File

@@ -8,8 +8,16 @@
position: relative;
}
.character-card .image {
.partyHeader {
display: inline-block;
}
.partyHeader iron-icon {
visibility: hidden;
}
.partyHeader:hover iron-icon{
visibility: initial;
}
.character-card .initials {

View File

@@ -10,31 +10,27 @@
{{#if currentUser}}
{{#if characters.count}}
<div class="character-list layout horizontal wrap">
{{# each characters}}
<a class="character-card flex layout vertical end-justified" href="{{pathFor route="characterSheet" data=this}}">
<iron-image class="fit {{colorClass}}"
sizing="cover" preload fade src={{picture}}>
</iron-image>
{{#unless picture}}
<div class="fit initials layout vertical center center-justified">
{{initials name}}
</div>
{{/unless}}
<paper-item>
<paper-item-body two-lines>
<div class="name white87">
{{name}}
</div>
<div secondary style="color: #8a8a8a; color: rgba(255,255,255,0.87);">
{{alignment}} {{gender}} {{race}}
</div>
</paper-item-body>
</paper-item>
<paper-ripple></paper-ripple>
</a>
{{# each charactersWithNoParty}}
{{> characterCard}}
{{/each}}
{{> gridPadding class="character-card flex layout vertical" num=12}}
</div>
{{# each party in parties}}
<div class="party" data-id={{party._id}}>
{{#with party}}
<div class="partyHeader clickable paper-font-title padded">
{{name}}
<iron-icon icon="create"></iron-icon>
</div>
{{/with}}
<div class="character-list layout horizontal wrap">
{{# each charactersInParty party._id}}
{{> characterCard}}
{{/each}}
{{> gridPadding class="character-card flex layout vertical" num=12}}
</div>
</div>
{{/each}}
{{else}}
<div layout vertical center center-justified class="padded">
<div>You don't seem to have any characters yet</div>
@@ -47,9 +43,46 @@
</div>
{{/if}}
<div class="fab-buffer"></div>
<paper-fab class="floatyButton addCharacter"
icon="add"
title="Add"></paper-fab>
{{#fabMenu}}
<div>
<paper-fab icon="social:group"
class="addParty"
mini>
</paper-fab>
<paper-tooltip position="left"> New Party </paper-tooltip>
</div>
<div>
<paper-fab icon="face"
class="addCharacter"
mini>
</paper-fab>
<paper-tooltip position="left"> New Character </paper-tooltip>
</div>
{{/fabMenu}}
</div>
</app-header-layout>
</template>
<template name="characterCard">
<a class="character-card flex layout vertical end-justified" href="{{characterPath this}}">
<iron-image class="fit {{colorClass}}"
sizing="cover" preload fade src={{picture}}>
</iron-image>
{{#unless picture}}
<div class="fit initials layout vertical center center-justified">
{{initials name}}
</div>
{{/unless}}
<paper-item>
<paper-item-body two-lines>
<div class="name white87">
{{name}}
</div>
<div secondary style="color: #8a8a8a; color: rgba(255,255,255,0.87);">
{{alignment}} {{gender}} {{race}}
</div>
</paper-item-body>
</paper-item>
<paper-ripple></paper-ripple>
</a>
</template>

View File

@@ -1,35 +1,57 @@
Template.characterList.helpers({
characters(){
characters() {
var userId = Meteor.userId();
return Characters.find(
{
$or: [
{readers: userId},
{writers: userId},
{owner: userId},
]
},
{
fields: {
name: 1,
urlName: 1,
picture: 1,
color: 1,
race: 1,
alignment: 1,
gender: 1,
},
sort: {name: 1},
}
{$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
{sort: {name: 1}}
);
},
parties() {
return Parties.find({owner: Meteor.userId()});
},
charactersInParty(partyId) {
var userId = Meteor.userId();
var party = Parties.findOne(partyId);
return Characters.find(
{
_id: {$in: party.characters},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
);
},
charactersWithNoParty() {
var userId = Meteor.userId();
var charArrays = Parties.find({owner: userId}).map(p => p.characters);
var partyChars = _.uniq(_.flatten(charArrays));
return Characters.find(
{
_id: {$nin: partyChars},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
);
},
});
Template.characterCard.helpers({
initials(name){
return name.replace(/[^A-Z]/g, "");
},
})
});
Template.characterList.events({
"tap .addCharacter": function(event, template) {
"click .partyHeader": function(event, instance){
pushDialogStack({
template: "partyDialog",
data: {
_id: this._id,
startEditing: true,
},
element: event.currentTarget.parentElement,
});
},
"click .addCharacter": function(event, instance) {
pushDialogStack({
template: "newCharacterDialog",
element: event.currentTarget,
@@ -37,8 +59,23 @@ Template.characterList.events({
if (!character) return;
character.owner = Meteor.userId();
let _id = Characters.insert(character);
Router.go("characterSheet", {_id});
let urlName = getSlug(character.name, {maintainCase: true}) || "-"
Router.go("characterSheet", {_id, urlName});
},
})
},
"click .addParty": function(event, instance) {
var partyId = Parties.insert({
owner: Meteor.userId(),
});
pushDialogStack({
template: "partyDialog",
data: {
_id: partyId,
startEditing: true,
},
element: event.currentTarget,
returnElement: instance.find(`.party[data-id='${partyId}']`),
});
},
});

View File

@@ -2,8 +2,21 @@
prevent character names from wrapping
*/
.character-name {
.side-list .character-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.side-list .partyHead {
font-weight: 500;
cursor: pointer;
}
.side-list .partyHead iron-icon {
transition: transform 0.3s ease;
}
.side-list .partyHead iron-icon.open {
transform: rotate(90deg);
}

View File

@@ -1,15 +1,31 @@
<template name="characterSideList">
{{#if characters.count}}
<div class="side-list">
{{#each characters}}
<a href={{pathFor route="characterSheet" data=this}} tabindex="-1" class="side-list-character characterRepresentative">
<paper-item class="short">
<div class="character-name">
{{name}}
</div>
</paper-item>
</a>
{{/each}}
</div>
{{/if}}
<div class="side-list">
{{#each charactersWithNoParty}}
<a href={{characterPath this}} tabindex="-1" class="side-list-character characterRepresentative">
<paper-item class="short">
<div class="character-name">
{{name}}
</div>
</paper-item>
</a>
{{/each}}
{{#each parties}}
<div class="paper-font-subhead partyHead">
<iron-icon icon="chevron-right" class="{{#if isOpen _id}}open{{/if}}">
</iron-icon>
{{name}}
</div>
<iron-collapse opened={{isOpen _id}}>
{{#each charactersInParty}}
<a href={{characterPath this}} tabindex="-1" class="side-list-character characterRepresentative">
<paper-item class="short">
<div class="character-name">
{{name}}
</div>
</paper-item>
</a>
{{/each}}
</iron-collapse>
{{/each}}
</div>
</template>

View File

@@ -1,33 +1,50 @@
Template.characterSideList.onCreated(function() {
this.subscribe("characterList");
this.openedParties = new ReactiveVar(new Set());
});
Template.characterSideList.helpers({
characters: function() {
parties() {
var userId = Meteor.userId();
return Parties.find({owner: userId});
},
charactersInParty() {
var userId = Meteor.userId();
return Characters.find(
{
$or: [
{readers: userId},
{writers: userId},
{owner: userId},
]
_id: {$in: this.characters},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{
fields: {name: 1, urlName: 1},
sort: {name: 1},
}
{sort: {name: 1}}
);
},
charactersWithNoParty() {
var userId = Meteor.userId();
var charArrays = Parties.find({owner: userId}).map(p => p.characters);
var partyChars = _.uniq(_.flatten(charArrays));
return Characters.find(
{
_id: {$nin: partyChars},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
);
},
isOpen(id) {
var openedParties = Template.instance().openedParties.get();
console.log(openedParties);
return openedParties.has(id);
},
});
Template.characterSideList.events({
"tap .singleLineItem": function(event, instance) {
//Router.go("characterSheet", {_id: this._id});
$("core-drawer-panel").get(0).closeDrawer();
},
"tap core-item": function() {
Router.go("characterList");
$("core-drawer-panel").get(0).closeDrawer();
"click .partyHead": function(event, instance){
var openedParties = instance.openedParties.get();
if (openedParties.has(this._id)){
openedParties.delete(this._id);
} else {
openedParties.add(this._id);
}
instance.openedParties.set(openedParties);
},
});

View File

@@ -0,0 +1,3 @@
.partyEdit .inPartyCheckbox {
margin-bottom: 8px;
}

View File

@@ -0,0 +1,32 @@
<template name="partyDialog">
{{#with party}}
{{#baseDialog title=name hideColor=true startEditing=true}}
{{> partyDetails}}
{{else}}
{{> partyEdit}}
{{/baseDialog}}
{{/with}}
</template>
<template name="partyDetails">
<div class="fit layout vertical partyDetails" style="padding: 24px;">
<div>
{{#each character in getCharacters}}
<div>{{character.name}}</div>
{{/each}}
</div>
</div>
</template>
<template name="partyEdit">
<div class="layout vertical partyEdit" style="padding: 24px;">
<paper-input class="partyNameInput" value={{name}} label="Party name">
</paper-input>
{{#each allCharacters}}
<paper-checkbox checked={{charInParty _id}}
class="inPartyCheckbox">
{{name}}
</paper-checkbox>
{{/each}}
</div>
</template>

View File

@@ -0,0 +1,62 @@
Template.partyDialog.helpers({
party(){
return Parties.findOne(this._id);
}
});
Template.partyDetails.helpers({
getCharacters (){
var userId = Meteor.userId();
return Characters.find(
{
_id: {$in: this.characters},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
);
}
});
Template.partyEdit.helpers({
allCharacters() {
var userId = Meteor.userId();
return Characters.find(
{$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
{sort: {name: 1}}
);
},
charInParty(charId) {
return _.contains(Template.parentData().characters, charId);
},
});
Template.partyDialog.events({
"click #deleteButton": function(event, instance){
Parties.remove(instance.data._id);
popDialogStack();
},
"click #doneEditingButton": function(event, instance){
popDialogStack();
},
});
Template.partyEdit.events({
"change .inPartyCheckbox": function(event, instance){
var currentCharacters = this.characters;
var checked = event.currentTarget.checked;
var charId = this._id;
var partyId = instance.data._id;
if (checked){
Parties.update(partyId, {$addToSet: {characters: charId}});
} else {
Parties.update(partyId, {$pull: {characters: charId}});
}
},
"input .partyNameInput": function(event, instance){
var name = event.currentTarget.value;
Parties.update(this._id, {$set: {name}}, {
removeEmptyStrings: false,
trimStrings: false,
});
},
});

View File

@@ -4,11 +4,13 @@ Template.baseDialog.onCreated(function(){
Template.baseDialog.helpers({
editing: function(){
if (!Template.parentData() || !Template.parentData().charId) return true;
return Template.instance().editing.get() &&
canEditCharacter(Template.parentData().charId);
},
showEdit: function() {
if (this.hideEdit) return false;
if (!Template.parentData() || !Template.parentData().charId) return true;
return canEditCharacter(Template.parentData().charId);
},
});

View File

@@ -23,142 +23,12 @@ Meteor.methods({
Migrations.add({
version: 1,
name: "converts effect proficiencies to proficiency objects, removes types from assets",
up: function() {
//convert proficiency effects to proficiency objects
Effects.find({operation: "proficiency"}).forEach(function(effect){
var type = "skill";
if (_.contains(SAVES, effect.stat)) type = "save";
Proficiencies.insert({
charId: effect.charId,
name: effect.stat,
value: effect.value,
parent: _.clone(effect.parent),
type: type,
enabled: effect.enabled,
}, function(err){
if (!err) Effects.remove(effect._id);
});
});
//store type as a parent group if it's needed
Effects.find({"parent.collection": "Characters"}).forEach(function(e){
Effects.update(e._id, {$set: {"parent.group": e.type}});
});
Attacks.find({"parent.collection": "Characters"}).forEach(function(a){
Attacks.update(a._id, {$set: {"parent.group": a.type}});
});
//remove type
Effects.update({}, {$unset: {type: ""}}, {validate: false, multi: true});
Attacks.update({}, {$unset: {type: ""}}, {validate: false, multi: true});
//remove languages and proficiencies
Characters.update(
{},
{$unset: {languages: "", proficiencies: ""}},
{validate: false, multi: true}
);
},
});
Migrations.add({
version: 2,
name: "Converts attacks from damage dice and damage bonus to a string with curly bracket calculations, adds settings.showIncrement to items",
up: function() {
//update attacks
Attacks.find({}).forEach(function(attack) {
if (!attack.damage && attack.damageDice && attack.damageBonus){
var newDamage = attack.damageDice +
" + {" + attack.damageBonus + "}";
Attacks.update(
attack._id,
{
$unset: {
damageBonus: "",
damageDice: "",
},
$set: {
damage: newDamage
},
},
{validate: false});
}
});
//update Items
Items.update(
{settings: undefined},
{$set: {"settings.showIncrement" : false}},
{validate: false, multi: true}
);
},
});
Migrations.add({
version: 3,
name: "Converts attacks from damage dice and damage bonus to a string with curly bracket calculations, adds settings.showIncrement to items",
up: function() {
//update characters
Characters.update(
{"settings.useVariantEncumbrance": undefined},
{$set: {"settings.useVariantEncumbrance" : false}},
{validate: false, multi: true}
);
Characters.update(
{"settings.useStandardEncumbrance": undefined},
{$set: {"settings.useStandardEncumbrance" : true}},
{validate: false, multi: true}
);
},
});
Migrations.add({
version: 4,
name: "Adds an effect to give characters a base carry capacity",
up: function() {
//update characters
Characters.find({}).forEach(function(char){
Characters.update(char._id, {
$set: {
carryMultiplier: {
adjustment: 0,
reset: "longRest",
}
}
});
var effect = Effects.findOne({
charId: char._id, name: "Natural Carrying Capacity",
});
if (effect) return;
Effects.insert({
charId: char._id,
name: "Natural Carrying Capacity",
stat: "carryMultiplier",
operation: "base",
value: "1",
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
effect = Effects.findOne({
charId: char._id, name: "Natural Carrying Capacity",
});
if (!effect) throw "Carry capacity effect should be set by now."
});
},
down: function(){
return;
},
});
Migrations.add({
version: 5,
name: "Gives all characters a URL name",
up: function() {
//update characters
Characters.find({}).forEach(function(char){
if (char.urlName) return;
var urlName = getSlug(char.name, {maintainCase: true});
var urlName = getSlug(char.name, {maintainCase: true}) || "-";
Characters.update(char._id, {$set: {urlName}});
});
},

View File

@@ -4,27 +4,24 @@ Meteor.publish("characterList", function(){
this.ready();
return;
}
return Characters.find(
{
$or: [
{readers: userId},
{writers: userId},
{owner: userId},
]
},
{
fields: {
name: 1,
urlName: 1,
race: 1,
alignment: 1,
gender: 1,
readers: 1,
writers:1,
owner: 1,
color: 1,
picture: 1,
return [
Characters.find(
{$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
{
fields: {
name: 1,
urlName: 1,
race: 1,
alignment: 1,
gender: 1,
readers: 1,
writers:1,
owner: 1,
color: 1,
picture: 1,
}
}
}
);
),
Parties.find({owner: userId}),
];
});