This commit is contained in:
parent
4b25f753f1
commit
18603dc783
@ -9,7 +9,7 @@ class TaskItemsController < ApplicationController
|
|||||||
@task_item.task_list = @task_list
|
@task_item.task_list = @task_list
|
||||||
|
|
||||||
if @task_item.save
|
if @task_item.save
|
||||||
render :show, status: :created, location: @task_item
|
render :show, status: :created, location: [@task_list, @task_item]
|
||||||
else
|
else
|
||||||
render json: @task_item.errors, status: :unprocessable_entity
|
render json: @task_item.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@ -17,7 +17,7 @@ class TaskItemsController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if @task_item.update(task_item_params)
|
if @task_item.update(task_item_params)
|
||||||
render :show, status: :ok, location: @task_item
|
render :show, status: :ok, location: [@task_list, @task_item]
|
||||||
else
|
else
|
||||||
render json: @task_item.errors, status: :unprocessable_entity
|
render json: @task_item.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@ -31,7 +31,7 @@ class TaskItemsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def task_item_params
|
def task_item_params
|
||||||
params.require(:task_item).permit(:name, :quantity)
|
params.require(:task_item).permit(:name, :quantity, :completed)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_task_list
|
def set_task_list
|
||||||
|
@ -4,7 +4,7 @@ class TaskListsController < ApplicationController
|
|||||||
before_action :set_task_list, only: [:show, :update, :destroy]
|
before_action :set_task_list, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@task_lists = TaskList.for_user(current_user).order(created_at: :desc)
|
@task_lists = TaskList.for_user(current_user).includes(:task_items).order(created_at: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -46,27 +46,7 @@
|
|||||||
created() {
|
created() {
|
||||||
if (this.user === null && this.authChecked === false) {
|
if (this.user === null && this.authChecked === false) {
|
||||||
this.checkAuthentication();
|
this.checkAuthentication();
|
||||||
}
|
|
||||||
|
|
||||||
// Hard coded values taken directly from Bulma css
|
|
||||||
const mediaQueries = {
|
|
||||||
mobile: "screen and (max-width: 768px)",
|
|
||||||
tablet: "screen and (min-width: 769px)",
|
|
||||||
tabletOnly: "screen and (min-width: 769px) and (max-width: 1023px)",
|
|
||||||
touch: "screen and (max-width: 1023px)",
|
|
||||||
desktop: "screen and (min-width: 1024px)",
|
|
||||||
desktopOnly: "screen and (min-width: 1024px) and (max-width: 1215px)",
|
|
||||||
widescreen: "screen and (min-width: 1216px)",
|
|
||||||
widescreenOnly: "screen and (min-width: 1216px) and (max-width: 1407px)",
|
|
||||||
fullhd: "screen and (min-width: 1408px)"
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let device in mediaQueries) {
|
|
||||||
const query = window.matchMedia(mediaQueries[device]);
|
|
||||||
query.onchange = (q) => {
|
|
||||||
this.$store.commit("setMediaQuery", {mediaName: device, value: q.matches});
|
|
||||||
};
|
|
||||||
query.onchange(query);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
<p class="modal-card-title">{{ title }}</p>
|
<p class="modal-card-title">{{ title }}</p>
|
||||||
<app-icon icon="x" aria-label="close" @click="close"></app-icon>
|
<app-icon class="close-button" icon="x" aria-label="close" @click="close"></app-icon>
|
||||||
</slot>
|
</slot>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
document.body.appendChild(this.$refs.modal);
|
this.$root.$el.appendChild(this.$refs.modal);
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
@ -63,6 +63,8 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-primary">Add</button>
|
<button class="button is-primary" @click="save">Add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -45,11 +45,15 @@
|
|||||||
|
|
||||||
save() {
|
save() {
|
||||||
this.$emit("save", this.taskItem);
|
this.$emit("save", this.taskItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.$refs.nameInput.focus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$refs.nameInput.focus();
|
this.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
<div>
|
<div>
|
||||||
|
|
||||||
<app-expand-transition name="fade">
|
<app-expand-transition name="fade">
|
||||||
<task-item-edit :task-item="newItem" v-if="showAddItem"></task-item-edit>
|
<task-item-edit @save="save" :task-item="newItem" v-if="showAddItem" ref="itemEdit"></task-item-edit>
|
||||||
</app-expand-transition>
|
</app-expand-transition>
|
||||||
|
|
||||||
<table class="table is-narrow">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
@ -17,8 +17,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="i in taskItems">
|
<tr v-for="i in taskItems" :key="i.id" @click="toggleItem(i)">
|
||||||
<td></td>
|
<td>
|
||||||
|
<div class="check">
|
||||||
|
<app-icon v-if="i.completed" icon="check"></app-icon>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>{{ i.name }}</td>
|
<td>{{ i.name }}</td>
|
||||||
<td>{{ i.quantity }}</td>
|
<td>{{ i.quantity }}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@ -36,40 +40,83 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import * as Errors from '../lib/Errors';
|
||||||
|
import { mapActions } from "vuex";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
|
|
||||||
import TaskItemEdit from "./TaskItemEdit";
|
import TaskItemEdit from "./TaskItemEdit";
|
||||||
|
|
||||||
const newItemTemplate = function() {
|
const newItemTemplate = function(listId) {
|
||||||
return {
|
return {
|
||||||
|
task_list_id: listId,
|
||||||
name: '',
|
name: '',
|
||||||
quantity: ''
|
quantity: '',
|
||||||
|
completed: false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
taskItems: {
|
taskList: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Array
|
type: Object
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showAddItem: false,
|
showAddItem: false,
|
||||||
newItem: newItemTemplate(),
|
newItem: null,
|
||||||
newItemValidationErrors: {}
|
newItemValidationErrors: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
taskItems() {
|
||||||
|
const top = [];
|
||||||
|
const bottom = [];
|
||||||
|
const list = (this.taskList ? this.taskList.task_items : []);
|
||||||
|
|
||||||
|
for (let i of list) {
|
||||||
|
if (!i.completed) {
|
||||||
|
top.push(i);
|
||||||
|
} else {
|
||||||
|
bottom.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return top.concat(bottom);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions([
|
||||||
|
'createTaskItem',
|
||||||
|
'updateTaskItem'
|
||||||
|
]),
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.loadResource(
|
||||||
|
this.createTaskItem(this.newItem)
|
||||||
|
.then(() => {
|
||||||
|
this.newItem = newItemTemplate(this.taskList.id);
|
||||||
|
this.$refs.itemEdit.focus();
|
||||||
|
})
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.newItemValidationErrors = err.validationErrors()))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleItem(i) {
|
||||||
|
const item = cloneDeep(i);
|
||||||
|
item.completed = !item.completed;
|
||||||
|
this.loadResource(
|
||||||
|
this.updateTaskItem(item)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
toggleShowAddItem() {
|
toggleShowAddItem() {
|
||||||
|
this.newItem = newItemTemplate(this.taskList.id);
|
||||||
this.showAddItem = !this.showAddItem;
|
this.showAddItem = !this.showAddItem;
|
||||||
if (this.showAddItem) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
//this.$refs.quantityInput.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<app-dropdown button-class="is-primary" :open="showListDropdown" :label="listSelectLabel" @open="showListDropdown = true" @close="showListDropdown = false">
|
<app-dropdown button-class="is-primary" :open="showListDropdown" :label="listSelectLabel" @open="showListDropdown = true" @close="showListDropdown = false">
|
||||||
|
|
||||||
<task-list-dropdown-item v-for="l in taskLists" :task-list="l" :active="currentList !== null && currentList.id === l.id" @select="selectList" @delete="deleteList"></task-list-dropdown-item>
|
<task-list-dropdown-item v-for="l in taskLists" :key="l.id" :task-list="l" :active="currentTaskList !== null && currentTaskList.id === l.id" @select="selectList" @delete="deleteList"></task-list-dropdown-item>
|
||||||
|
|
||||||
<hr class="dropdown-divider" v-if="taskLists.length > 0">
|
<hr class="dropdown-divider" v-if="taskLists.length > 0">
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
@ -12,9 +12,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</app-dropdown>
|
</app-dropdown>
|
||||||
|
|
||||||
<div v-if="currentList !== null">
|
<div v-if="currentTaskList !== null">
|
||||||
|
|
||||||
<task-item-list :task-items="currentList.task_items"></task-item-list>
|
<task-item-list :task-list="currentTaskList"></task-item-list>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -25,6 +25,8 @@
|
|||||||
|
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
import * as Errors from '../lib/Errors';
|
import * as Errors from '../lib/Errors';
|
||||||
|
import { mapActions, mapMutations, mapState } from "vuex";
|
||||||
|
|
||||||
import TaskListMiniForm from "./TaskListMiniForm";
|
import TaskListMiniForm from "./TaskListMiniForm";
|
||||||
import TaskListDropdownItem from "./TaskListDropdownItem";
|
import TaskListDropdownItem from "./TaskListDropdownItem";
|
||||||
import TaskItemList from "./TaskItemList";
|
import TaskItemList from "./TaskItemList";
|
||||||
@ -38,66 +40,61 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
taskLists: [],
|
|
||||||
showListDropdown: false,
|
showListDropdown: false,
|
||||||
currentList: null,
|
|
||||||
newList: newListTemplate(),
|
newList: newListTemplate(),
|
||||||
newListValidationErrors: {}
|
newListValidationErrors: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'taskLists',
|
||||||
|
'currentTaskList'
|
||||||
|
]),
|
||||||
listSelectLabel() {
|
listSelectLabel() {
|
||||||
if (this.currentList === null) {
|
if (this.currentTaskList === null) {
|
||||||
return "Select or Create a List";
|
return "Select or Create a List";
|
||||||
} else {
|
} else {
|
||||||
return this.currentList.name;
|
return this.currentTaskList.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions([
|
||||||
|
'refreshTaskLists',
|
||||||
|
'createTaskList',
|
||||||
|
'deleteTaskList'
|
||||||
|
]),
|
||||||
|
...mapMutations([
|
||||||
|
'setCurrentTaskList'
|
||||||
|
]),
|
||||||
|
|
||||||
selectList(list) {
|
selectList(list) {
|
||||||
this.currentList = list;
|
this.setCurrentTaskList(list);
|
||||||
this.showListDropdown = false;
|
this.showListDropdown = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
saveNewList() {
|
saveNewList() {
|
||||||
this.loadResource(
|
this.loadResource(
|
||||||
api.postTaskList(this.newList)
|
this.createTaskList(this.newList)
|
||||||
.then(l => this.currentList = l)
|
|
||||||
.then(() => this.showListDropdown = false)
|
.then(() => this.showListDropdown = false)
|
||||||
.then(() => this.updateData())
|
|
||||||
.then(() => { this.newList = newListTemplate(); this.newListValidationErrors = {}; } )
|
.then(() => { this.newList = newListTemplate(); this.newListValidationErrors = {}; } )
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.newListValidationErrors = err.validationErrors()))
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.newListValidationErrors = err.validationErrors()))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateData() {
|
|
||||||
return this.loadResource(api.getTaskLists(data => this.setNewData(data)));
|
|
||||||
},
|
|
||||||
|
|
||||||
setNewData(list) {
|
|
||||||
this.taskLists = list;
|
|
||||||
if (this.currentList !== null) {
|
|
||||||
this.currentList = this.taskLists.find(l => this.currentList.id === l.id) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentList === null && this.taskLists.length > 0) {
|
|
||||||
this.currentList = this.taskLists[0];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteList(list) {
|
deleteList(list) {
|
||||||
this.loadResource(
|
this.loadResource(
|
||||||
api.deleteTaskList(list)
|
this.deleteTaskList(list)
|
||||||
.then(() => this.updateData())
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.updateData()
|
this.loadResource(
|
||||||
|
this.refreshTaskLists()
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
<app-modal title="Login" :open="showLogin" @dismiss="showLogin = false">
|
<app-modal title="Login" :open="showLogin" @dismiss="showLogin = false">
|
||||||
<div>
|
<div>
|
||||||
<form @submit.prevent="login">
|
<form @submit.prevent="performLogin">
|
||||||
|
|
||||||
<div v-if="error" class="notification is-danger">
|
<div v-if="loginMessage" class="notification is-danger">
|
||||||
{{error}}
|
{{loginMessage}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
import { mapMutations } from "vuex";
|
import { mapActions, mapState } from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@ -61,14 +61,17 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'loginMessage'
|
||||||
|
]),
|
||||||
enableSubmit() {
|
enableSubmit() {
|
||||||
return this.username !== '' && this.password !== '' && !this.isLoading;
|
return this.username !== '' && this.password !== '' && !this.isLoading;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations([
|
...mapActions([
|
||||||
'setUser'
|
'login'
|
||||||
]),
|
]),
|
||||||
|
|
||||||
openDialog() {
|
openDialog() {
|
||||||
@ -76,16 +79,18 @@
|
|||||||
this.$nextTick(() => this.$refs.usernameInput.focus());
|
this.$nextTick(() => this.$refs.usernameInput.focus());
|
||||||
},
|
},
|
||||||
|
|
||||||
login() {
|
performLogin() {
|
||||||
if (this.username !== '' && this.password !== '') {
|
if (this.username !== '' && this.password !== '') {
|
||||||
this.loadResource(api.postLogin(this.username, this.password).then(data => {
|
const params = {username: this.username, password: this.password};
|
||||||
if (data.success) {
|
|
||||||
this.setUser(data.user);
|
this.loadResource(
|
||||||
this.showLogin = false;
|
this.login(params)
|
||||||
} else {
|
.then(data => {
|
||||||
this.error = data.message;
|
if (data.success) {
|
||||||
}
|
this.showLogin = false;
|
||||||
}));
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -387,13 +387,15 @@ class Api {
|
|||||||
return {
|
return {
|
||||||
task_item: {
|
task_item: {
|
||||||
name: taskItem.name,
|
name: taskItem.name,
|
||||||
quantity: taskItem.quantity
|
quantity: taskItem.quantity,
|
||||||
|
completed: taskItem.completed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
postTaskItem(listId, taskItem) {
|
postTaskItem(listId, taskItem) {
|
||||||
return this.post(`/task_lists/${listId}/task_items`)
|
const params = this.buildTaskItemParams(taskItem);
|
||||||
|
return this.post(`/task_lists/${listId}/task_items`, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
patchTaskItem(listId, taskItem) {
|
patchTaskItem(listId, taskItem) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { mapGetters, mapMutations, mapState } from 'vuex';
|
import { mapActions, mapGetters, mapMutations, mapState } from 'vuex';
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
|
|
||||||
Vue.mixin({
|
Vue.mixin({
|
||||||
@ -23,10 +23,12 @@ Vue.mixin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions([
|
||||||
|
'updateCurrentUser'
|
||||||
|
]),
|
||||||
...mapMutations([
|
...mapMutations([
|
||||||
'setError',
|
'setError',
|
||||||
'setLoading',
|
'setLoading'
|
||||||
'setUser'
|
|
||||||
]),
|
]),
|
||||||
|
|
||||||
loadResource(promise) {
|
loadResource(promise) {
|
||||||
@ -43,7 +45,7 @@ Vue.mixin({
|
|||||||
},
|
},
|
||||||
|
|
||||||
checkAuthentication() {
|
checkAuthentication() {
|
||||||
return this.loadResource(api.getCurrentUser().then(user => this.setUser(user)));
|
return this.loadResource(this.updateCurrentUser());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
44
app/javascript/lib/VuexResponsiveSync.js
Normal file
44
app/javascript/lib/VuexResponsiveSync.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Adds a module to a vuex store with a set of media query states
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
module: "mediaQueries"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hard coded values taken directly from Bulma css
|
||||||
|
const mediaQueries = {
|
||||||
|
mobile: "screen and (max-width: 768px)",
|
||||||
|
tablet: "screen and (min-width: 769px)",
|
||||||
|
tabletOnly: "screen and (min-width: 769px) and (max-width: 1023px)",
|
||||||
|
touch: "screen and (max-width: 1023px)",
|
||||||
|
desktop: "screen and (min-width: 1024px)",
|
||||||
|
desktopOnly: "screen and (min-width: 1024px) and (max-width: 1215px)",
|
||||||
|
widescreen: "screen and (min-width: 1216px)",
|
||||||
|
widescreenOnly: "screen and (min-width: 1216px) and (max-width: 1407px)",
|
||||||
|
fullhd: "screen and (min-width: 1408px)"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function(store, options) {
|
||||||
|
let opts = Object.assign({}, defaultOptions, options || {});
|
||||||
|
const moduleName = opts.module;
|
||||||
|
|
||||||
|
const initialState = {};
|
||||||
|
|
||||||
|
for (let device in mediaQueries) {
|
||||||
|
const query = window.matchMedia(mediaQueries[device]);
|
||||||
|
query.onchange = (q) => {
|
||||||
|
store.commit(moduleName + "/MEDIA_QUERY_CHANGED", {mediaName: device, value: q.matches});
|
||||||
|
};
|
||||||
|
initialState[device] = query.matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.registerModule(moduleName, {
|
||||||
|
namespaced: true,
|
||||||
|
state: initialState,
|
||||||
|
mutations: {
|
||||||
|
"MEDIA_QUERY_CHANGED" (state, data) {
|
||||||
|
state[data.mediaName] = data.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
@ -3,6 +3,7 @@ import '../styles';
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { sync } from 'vuex-router-sync';
|
import { sync } from 'vuex-router-sync';
|
||||||
import { swInit } from "../lib/ServiceWorker";
|
import { swInit } from "../lib/ServiceWorker";
|
||||||
|
import responsiveSync from "../lib/VuexResponsiveSync";
|
||||||
import VueProgressBar from "vue-progressbar";
|
import VueProgressBar from "vue-progressbar";
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
@ -57,6 +58,7 @@ Vue.use(VueProgressBar, {
|
|||||||
|
|
||||||
sync(store, router);
|
sync(store, router);
|
||||||
swInit(store);
|
swInit(store);
|
||||||
|
responsiveSync(store);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
@ -13,6 +13,10 @@ export default new Vuex.Store({
|
|||||||
error: null,
|
error: null,
|
||||||
authChecked: false,
|
authChecked: false,
|
||||||
user: null,
|
user: null,
|
||||||
|
loginMessage: null,
|
||||||
|
|
||||||
|
taskLists: [],
|
||||||
|
currentTaskList: null,
|
||||||
|
|
||||||
// MediaQueryList objects in the root App component maintain this state.
|
// MediaQueryList objects in the root App component maintain this state.
|
||||||
mediaQueries: {
|
mediaQueries: {
|
||||||
@ -62,14 +66,125 @@ export default new Vuex.Store({
|
|||||||
state.user = user;
|
state.user = user;
|
||||||
},
|
},
|
||||||
|
|
||||||
setMediaQuery(state, data) {
|
setLoginMessage(state, msg) {
|
||||||
state.mediaQueries[data.mediaName] = data.value;
|
state.loginMessage = msg;
|
||||||
|
},
|
||||||
|
|
||||||
|
setTaskLists(state, lists) {
|
||||||
|
state.taskLists = lists || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
setCurrentTaskList(state, list) {
|
||||||
|
state.currentTaskList = list || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
appendTaskItem(state, item) {
|
||||||
|
const listId = item.task_list_id;
|
||||||
|
const list = state.taskLists.find(l => l.id === listId);
|
||||||
|
if (list) {
|
||||||
|
list.task_items.push(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
replaceTaskItem(state, item) {
|
||||||
|
const listId = item.task_list_id;
|
||||||
|
const list = state.taskLists.find(l => l.id === listId);
|
||||||
|
if (list) {
|
||||||
|
const taskIdx = list.task_items.findIndex(i => i.id === item.id);
|
||||||
|
if (taskIdx >= 0) {
|
||||||
|
list.task_items.splice(taskIdx, 1, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTaskItem(state, item) {
|
||||||
|
const listId = item.task_list_id;
|
||||||
|
const list = state.taskLists.find(l => l.id === listId);
|
||||||
|
if (list) {
|
||||||
|
const taskIdx = list.task_items.findIndex(i => i.id === item.id);
|
||||||
|
if (taskIdx >= 0) {
|
||||||
|
list.task_items.splice(taskIdx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
updateCurrentUser({commit}) {
|
||||||
|
return api.getCurrentUser()
|
||||||
|
.then(user => {
|
||||||
|
commit("setUser", user);
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
login({commit}, authData) {
|
||||||
|
return api.postLogin(authData.username, authData.password)
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
commit("setUser", data.user);
|
||||||
|
commit("setLoginMessage", null);
|
||||||
|
} else {
|
||||||
|
commit("setUser", null);
|
||||||
|
commit("setLoginMessage", data.message);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
logout({commit}) {
|
logout({commit}) {
|
||||||
return api.getLogout()
|
return api.getLogout()
|
||||||
.then(() => commit("setUser", null));
|
.then(() => {
|
||||||
|
commit("setUser", null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshTaskLists({commit, state}) {
|
||||||
|
const cb = function(data) {
|
||||||
|
commit("setTaskLists", data);
|
||||||
|
let ctl = null;
|
||||||
|
|
||||||
|
if (state.currentTaskList) {
|
||||||
|
ctl = data.find(l => l.id === state.currentTaskList.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl = ctl || data[0] || null;
|
||||||
|
commit("setCurrentTaskList", ctl);
|
||||||
|
};
|
||||||
|
|
||||||
|
return api.getTaskLists(cb)
|
||||||
|
},
|
||||||
|
|
||||||
|
createTaskList({commit, dispatch}, newList) {
|
||||||
|
return api.postTaskList(newList)
|
||||||
|
.then(data => commit("setCurrentTaskList", data))
|
||||||
|
.then(() => dispatch("refreshTaskLists"))
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteTaskList({dispatch}, taskList) {
|
||||||
|
return api.deleteTaskList(taskList)
|
||||||
|
.then(() => dispatch("refreshTaskLists"));
|
||||||
|
},
|
||||||
|
|
||||||
|
createTaskItem({commit, dispatch}, taskItem) {
|
||||||
|
|
||||||
|
return api.postTaskItem(taskItem.task_list_id, taskItem)
|
||||||
|
.then(data => {
|
||||||
|
commit("appendTaskItem", data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTaskItem({commit}, taskItem) {
|
||||||
|
return api.patchTaskItem(taskItem.task_list_id, taskItem)
|
||||||
|
.then(data => {
|
||||||
|
commit("replaceTaskItem", data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteTaskItem({commit}, taskItem) {
|
||||||
|
return api.deleteTaskItem(taskItem.task_list_id, taskItem)
|
||||||
|
.then(() => commit("removeTaskItem", taskItem));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
95
app/models/concerns/default_values.rb
Normal file
95
app/models/concerns/default_values.rb
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# == Default Values
|
||||||
|
#
|
||||||
|
# Allows default attributes values to be declared. The callback used to set the values can be controlled via options.
|
||||||
|
#
|
||||||
|
# The following option keys are allowed:
|
||||||
|
# `:on`. If omitted, it defaults to :initialize.
|
||||||
|
# :on may be one of the following: :initialize, :create, :update, :save
|
||||||
|
#
|
||||||
|
# `:empty`. If omitted, it defaults to only updating `nil` values
|
||||||
|
# :empty should be a lambda (or proc) that accepts the current value of the attribute and returns true if the value
|
||||||
|
# should be replaced by the default
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# Sets `attr` to 'default' and `attr2` to 'default' any time the class is instantiated
|
||||||
|
# default_values {
|
||||||
|
# attr: 'default',
|
||||||
|
# attr2: 'default'
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Sets `roles` to [:super_admin] only when saving a new object and only if roles is an empty array (it will not update a nil value)
|
||||||
|
# default_values({roles: [:super_admin]}, {on: :create, empty: ->(v) { v == [] }})
|
||||||
|
#
|
||||||
|
module DefaultValues
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
DEFAULT_OPTIONS = { on: :initialize }
|
||||||
|
|
||||||
|
DefaultValue = Struct.new(:value, :options)
|
||||||
|
|
||||||
|
included do
|
||||||
|
class_attribute :_default_values
|
||||||
|
self._default_values = {}
|
||||||
|
|
||||||
|
after_initialize :set_default_values_initialize
|
||||||
|
before_create :set_default_values_create
|
||||||
|
before_update :set_default_values_update
|
||||||
|
before_save :set_default_values_save
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_default_values_initialize
|
||||||
|
set_default_values(:initialize)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_default_values_create
|
||||||
|
set_default_values(:create)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_default_values_update
|
||||||
|
set_default_values(:update)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_default_values_save
|
||||||
|
set_default_values(:save)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_default_values(on)
|
||||||
|
_default_values.each do |k, dv|
|
||||||
|
if dv.options[:on].to_sym == on
|
||||||
|
v = dv.value
|
||||||
|
v_lambda = Proc === v ? v : -> { v }
|
||||||
|
|
||||||
|
tester = dv.options[:empty] || ->(x) { x.nil? }
|
||||||
|
|
||||||
|
self.send("#{k}=", self.instance_exec(&v_lambda)) if self.respond_to?(k) && tester.call(self.send(k))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
# Copy defaults on inheritance.
|
||||||
|
def inherited(base)
|
||||||
|
base._default_values = _default_values.dup
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_values(defaults_hash, options = {})
|
||||||
|
options = DEFAULT_OPTIONS.merge(options.symbolize_keys)
|
||||||
|
|
||||||
|
valid_ons = [:initialize, :create, :update, :save]
|
||||||
|
|
||||||
|
unless valid_ons.include? options[:on]
|
||||||
|
raise "Invalid options[:on] value: [#{options[:on]}]. Must be one of these symbols: #{valid_ons.join(', ')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:empty].present?
|
||||||
|
proc = options[:empty]
|
||||||
|
raise "Invalid options[:empty]. Must be a Proc or Lambda with an arity of 1" unless (proc.is_a?(Proc) && proc.arity == 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defaults_hash.each do |k, v|
|
||||||
|
_default_values[k] = DefaultValue.new(v, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -134,8 +134,6 @@ class Recipe < ApplicationRecord
|
|||||||
query = query.where(id: tags.joins(:recipes).pluck('recipes.id'))
|
query = query.where(id: tags.joins(:recipes).pluck('recipes.id'))
|
||||||
end
|
end
|
||||||
|
|
||||||
puts criteria.inspect
|
|
||||||
|
|
||||||
query.page(criteria.page).per(criteria.per)
|
query.page(criteria.page).per(criteria.per)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
class TaskItem < ApplicationRecord
|
class TaskItem < ApplicationRecord
|
||||||
|
include DefaultValues
|
||||||
|
|
||||||
belongs_to :task_list
|
belongs_to :task_list
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
|
default_values completed: false
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -14,8 +14,6 @@ module ViewModels
|
|||||||
self.send(setter, params[attr])
|
self.send(setter, params[attr])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
puts self.inspect
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def sort_column
|
def sort_column
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
<%
|
<%
|
||||||
pack_assets = [asset_pack_path("application.js"), asset_pack_path("application.css")].select { |a| a.present? }
|
manifest_data = Webpacker::manifest.refresh
|
||||||
|
pack_assets = manifest_data.values.select { |asset| asset !~ /\.map$/ } # [asset_pack_path("application.js"), asset_pack_path("application.css")].select { |a| a.present? }
|
||||||
%>
|
%>
|
||||||
|
|
||||||
var cacheName = "parsley-cache-<%= File.mtime(Webpacker::manifest.config.public_manifest_path).to_i %>";
|
var cacheName = "parsley-cache-<%= File.mtime(Webpacker::config.public_manifest_path).to_i %>";
|
||||||
|
|
||||||
var staticAssets = [
|
var staticAssets = [
|
||||||
"/"
|
"/"
|
||||||
@ -39,6 +40,11 @@ self.addEventListener('fetch', function(event) {
|
|||||||
var isCacheThenNetwork = event.request.headers.get("Cache-Then-Network") === "true";
|
var isCacheThenNetwork = event.request.headers.get("Cache-Then-Network") === "true";
|
||||||
var x, asset;
|
var x, asset;
|
||||||
|
|
||||||
|
// Any non-GET or non-http(s) request should be ignored
|
||||||
|
if (event.request.method !== 'GET' || event.request.url.indexOf('http') !== 0) {
|
||||||
|
return fetch(event.request);
|
||||||
|
}
|
||||||
|
|
||||||
// Cache-first response for static assets
|
// Cache-first response for static assets
|
||||||
for (x = 0; x < staticAssets.length; x++) {
|
for (x = 0; x < staticAssets.length; x++) {
|
||||||
asset = staticAssets[x];
|
asset = staticAssets[x];
|
||||||
|
@ -1 +1 @@
|
|||||||
json.extract! task_item, :id, :name, :quantity, :created_on, :updated_on
|
json.extract! task_item, :id, :task_list_id, :name, :quantity, :completed, :created_at, :updated_at
|
1
app/views/task_items/show.json.jbuilder
Normal file
1
app/views/task_items/show.json.jbuilder
Normal file
@ -0,0 +1 @@
|
|||||||
|
json.partial! 'task_items/task_item', task_item: @task_item
|
@ -1,3 +1,3 @@
|
|||||||
|
|
||||||
json.extract! task_list, :id, :name, :created_at, :updated_at
|
json.extract! task_list, :id, :name, :created_at, :updated_at
|
||||||
json.task_items task_list.task_items, partial: 'task_item/task_item', as: :task_item
|
json.task_items task_list.task_items, partial: 'task_items/task_item', as: :task_item
|
||||||
|
@ -34,7 +34,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
resources :task_lists, only: [:index, :show, :create, :update, :destroy] do
|
resources :task_lists, only: [:index, :show, :create, :update, :destroy] do
|
||||||
resources :task_list_items, only: [:create, :update, :destroy]
|
resources :task_items, only: [:create, :update, :destroy]
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :user, only: [:new, :create, :edit, :update]
|
resource :user, only: [:new, :create, :edit, :update]
|
||||||
|
@ -3,7 +3,7 @@ const vue = require('./loaders/vue');
|
|||||||
const svg = require('./loaders/svg');
|
const svg = require('./loaders/svg');
|
||||||
|
|
||||||
environment.loaders.append('vue', vue);
|
environment.loaders.append('vue', vue);
|
||||||
//environment.loaders.append('svg', svg);
|
//environment.loaders.prepend('svg', svg);
|
||||||
|
|
||||||
//const fileLoader = environment.loaders.get('file');
|
//const fileLoader = environment.loaders.get('file');
|
||||||
//fileLoader.exclude = /\.(svg)$/i;
|
//fileLoader.exclude = /\.(svg)$/i;
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
use: [{
|
use: [{
|
||||||
loader: 'svg-loader'
|
loader: 'url-loader',
|
||||||
|
options: {
|
||||||
|
limit: 10000
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
13
db/migrate/20180906191333_update_task_items.rb
Normal file
13
db/migrate/20180906191333_update_task_items.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class UpdateTaskItems < ActiveRecord::Migration[5.2]
|
||||||
|
|
||||||
|
class TaskItemMigrator < ActiveRecord::Base
|
||||||
|
self.table_name = 'task_items'
|
||||||
|
end
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_column :task_items, :completed, :boolean
|
||||||
|
|
||||||
|
TaskItemMigrator.reset_column_information
|
||||||
|
TaskItemMigrator.update_all(completed: false)
|
||||||
|
end
|
||||||
|
end
|
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2018_08_27_215102) do
|
ActiveRecord::Schema.define(version: 2018_09_06_191333) do
|
||||||
|
|
||||||
create_table "ingredient_units", force: :cascade do |t|
|
create_table "ingredient_units", force: :cascade do |t|
|
||||||
t.integer "ingredient_id", null: false
|
t.integer "ingredient_id", null: false
|
||||||
@ -132,6 +132,7 @@ ActiveRecord::Schema.define(version: 2018_08_27_215102) do
|
|||||||
t.string "quantity"
|
t.string "quantity"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.boolean "completed"
|
||||||
t.index ["task_list_id"], name: "index_task_items_on_task_list_id"
|
t.index ["task_list_id"], name: "index_task_items_on_task_list_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
25
lib/tasks/dev.rake
Normal file
25
lib/tasks/dev.rake
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
namespace :dev do
|
||||||
|
|
||||||
|
desc 'Run both rails server and webpack dev server'
|
||||||
|
task :run do
|
||||||
|
|
||||||
|
pids = []
|
||||||
|
|
||||||
|
pids << fork do
|
||||||
|
exec 'rails s'
|
||||||
|
end
|
||||||
|
|
||||||
|
pids << fork do
|
||||||
|
exec 'bin/webpack-dev-server'
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
Process.wait
|
||||||
|
rescue SignalException
|
||||||
|
puts 'shutting down...'
|
||||||
|
pids.each { |pid| Process.kill("SIGINT", pid) }
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -7,6 +7,7 @@
|
|||||||
"css-loader": "^0.28.11",
|
"css-loader": "^0.28.11",
|
||||||
"lodash": "^4.17.5",
|
"lodash": "^4.17.5",
|
||||||
"svg-loader": "^0.0.2",
|
"svg-loader": "^0.0.2",
|
||||||
|
"url-loader": "1.0.1",
|
||||||
"vue": "^2.5.16",
|
"vue": "^2.5.16",
|
||||||
"vue-loader": "^14.2.2",
|
"vue-loader": "^14.2.2",
|
||||||
"vue-progressbar": "^0.7.4",
|
"vue-progressbar": "^0.7.4",
|
||||||
|
Loading…
Reference in New Issue
Block a user