update dependencies; live task editing

This commit is contained in:
Dan Elbert 2022-02-02 21:12:27 -06:00
parent a306f72215
commit dd915624a3
21 changed files with 1485 additions and 1290 deletions

1
.foreman Normal file
View File

@ -0,0 +1 @@
port: 3000

View File

@ -1,9 +1,9 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gem 'rails', '6.1.3.2' gem 'rails', '6.1.4.4'
gem 'pg', '~> 1.2.3' gem 'pg', '~> 1.2.3'
gem 'webpacker', '5.3.0' gem 'webpacker', '5.4.3'
gem 'bootsnap', '>= 1.1.0', require: false gem 'bootsnap', '>= 1.1.0', require: false
gem 'oj', '~> 3.11.5' gem 'oj', '~> 3.11.5'

View File

@ -1,60 +1,60 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.3.2) actioncable (6.1.4.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.3.2) actionmailbox (6.1.4.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.4)
activejob (= 6.1.3.2) activejob (= 6.1.4.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.3.2) actionmailer (6.1.4.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.4)
actionview (= 6.1.3.2) actionview (= 6.1.4.4)
activejob (= 6.1.3.2) activejob (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.3.2) actionpack (6.1.4.4)
actionview (= 6.1.3.2) actionview (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.2) actiontext (6.1.4.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.3.2) actionview (6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.3.2) activejob (6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.3.2) activemodel (6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
activerecord (6.1.3.2) activerecord (6.1.4.4)
activemodel (= 6.1.3.2) activemodel (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
activestorage (6.1.3.2) activestorage (6.1.4.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.4)
activejob (= 6.1.3.2) activejob (= 6.1.4.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
marcel (~> 1.0.0) marcel (~> 1.0.0)
mini_mime (~> 1.0.2) mini_mime (>= 1.1.0)
activesupport (6.1.3.2) activesupport (6.1.4.4)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -127,7 +127,7 @@ GEM
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
method_source (1.0.0) method_source (1.0.0)
mini_mime (1.0.3) mini_mime (1.1.2)
mini_portile2 (2.7.1) mini_portile2 (2.7.1)
minitest (5.15.0) minitest (5.15.0)
msgpack (1.4.4) msgpack (1.4.4)
@ -153,20 +153,20 @@ GEM
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (6.1.3.2) rails (6.1.4.4)
actioncable (= 6.1.3.2) actioncable (= 6.1.4.4)
actionmailbox (= 6.1.3.2) actionmailbox (= 6.1.4.4)
actionmailer (= 6.1.3.2) actionmailer (= 6.1.4.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.4)
actiontext (= 6.1.3.2) actiontext (= 6.1.4.4)
actionview (= 6.1.3.2) actionview (= 6.1.4.4)
activejob (= 6.1.3.2) activejob (= 6.1.4.4)
activemodel (= 6.1.3.2) activemodel (= 6.1.4.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.3.2) railties (= 6.1.4.4)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
@ -177,11 +177,11 @@ GEM
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2) rails-html-sanitizer (1.4.2)
loofah (~> 2.3) loofah (~> 2.3)
railties (6.1.3.2) railties (6.1.4.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.4)
method_source method_source
rake (>= 0.8.7) rake (>= 0.13)
thor (~> 1.0) thor (~> 1.0)
rake (13.0.6) rake (13.0.6)
rb-fsevent (0.11.0) rb-fsevent (0.11.0)
@ -231,7 +231,7 @@ GEM
memoizable (~> 0.4) memoizable (~> 0.4)
parslet (~> 1.5) parslet (~> 1.5)
signed_multiset (~> 0.2) signed_multiset (~> 0.2)
webpacker (5.3.0) webpacker (5.4.3)
activesupport (>= 5.2) activesupport (>= 5.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
@ -256,14 +256,14 @@ DEPENDENCIES
oj (~> 3.11.5) oj (~> 3.11.5)
pg (~> 1.2.3) pg (~> 1.2.3)
puma (~> 5.3) puma (~> 5.3)
rails (= 6.1.3.2) rails (= 6.1.4.4)
rails-controller-testing rails-controller-testing
redcarpet (~> 3.5.1) redcarpet (~> 3.5.1)
rspec-rails (~> 5.0.1) rspec-rails (~> 5.0.1)
sqlite3 (~> 1.4.2) sqlite3 (~> 1.4.2)
tzinfo-data tzinfo-data
unitwise (~> 2.2.0) unitwise (~> 2.2.0)
webpacker (= 5.3.0) webpacker (= 5.4.3)
BUNDLED WITH BUNDLED WITH
2.2.17 2.2.17

View File

@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@ -0,0 +1,18 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.encrypted['_parsley_session']['user_id'])
verified_user
else
reject_unauthorized_connection
end
end
end
end

View File

@ -0,0 +1,12 @@
class TaskChannel < ApplicationCable::Channel
def subscribed
stream_for current_user.id
end
def self.update_task_list(task_list)
task_list.reload
self.broadcast_to task_list.user_id, { task_list: TaskListSerializer.for(task_list), action: 'updated' }
end
end

View File

@ -9,6 +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
TaskChannel.update_task_list(@task_list)
render json: TaskItemSerializer.for(@task_item), status: :created, location: [@task_list, @task_item] render json: TaskItemSerializer.for(@task_item), 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
@ -17,6 +18,7 @@ class TaskItemsController < ApplicationController
def update def update
if @task_item.update(task_item_params) if @task_item.update(task_item_params)
TaskChannel.update_task_list(@task_list)
render json: TaskItemSerializer.for(@task_item), status: :ok, location: [@task_list, @task_item] render json: TaskItemSerializer.for(@task_item), 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
@ -30,6 +32,8 @@ class TaskItemsController < ApplicationController
@task_items.each { |i| i.destroy } @task_items.each { |i| i.destroy }
end end
TaskChannel.update_task_list(@task_list)
head :no_content head :no_content
end end
@ -41,6 +45,8 @@ class TaskItemsController < ApplicationController
@task_items.each { |i| i.update_attribute(:completed, new_status) } @task_items.each { |i| i.update_attribute(:completed, new_status) }
end end
TaskChannel.update_task_list(@task_list)
head :no_content head :no_content
end end

View File

@ -27,6 +27,7 @@ class TaskListsController < ApplicationController
def update def update
ensure_owner(@task_list) do ensure_owner(@task_list) do
if @task_list.update(task_list_params) if @task_list.update(task_list_params)
TaskChannel.update_task_list(@task_list)
render json: TaskListSerializer.for(@task_list), status: :ok, location: @task_list render json: TaskListSerializer.for(@task_list), status: :ok, location: @task_list
else else
render json: @task_list.errors, status: :unprocessable_entity render json: @task_list.errors, status: :unprocessable_entity
@ -46,6 +47,7 @@ class TaskListsController < ApplicationController
recipe = Recipe.find(params[:recipe_id]) recipe = Recipe.find(params[:recipe_id])
@task_list.add_recipe_ingredients(recipe) @task_list.add_recipe_ingredients(recipe)
TaskChannel.update_task_list(@task_list)
head :no_content head :no_content
end end

View File

@ -53,6 +53,12 @@
watch: { watch: {
routeQuery() { routeQuery() {
this.refreshData(); this.refreshData();
},
recipe(newRecipe) {
if (newRecipe) {
document.title = `${newRecipe.name}`;
}
} }
}, },

View File

@ -0,0 +1,17 @@
import * as cable from "@rails/actioncable";
let consumer = null;
function createChannel(baseUrl, ...args) {
if (consumer === null) {
if (baseUrl !== null && baseUrl.toString() !== "") {
consumer = cable.createConsumer(baseUrl);
} else {
consumer = cable.createConsumer();
}
}
return consumer.subscriptions.create(...args);
}
export { createChannel };

View File

@ -5,6 +5,7 @@ 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 responsiveSync from "../lib/VuexResponsiveSync";
import { createChannel } from "../lib/ActionCable";
import VueProgressBar from "vue-progressbar"; import VueProgressBar from "vue-progressbar";
import VueResize from "vue-resize"; import VueResize from "vue-resize";
import config from '../config'; import config from '../config';
@ -67,6 +68,10 @@ Vue.use(VueResize);
sync(store, router); sync(store, router);
responsiveSync(store); responsiveSync(store);
Vue.prototype.$createChannel = function(...args) {
createChannel(null, ...args);
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const app = document.getElementById('app'); const app = document.getElementById('app');

View File

@ -40,6 +40,10 @@ router.afterEach((to, from) => {
if (to.meta.handleInitialLoad !== true && $store.state.initialLoad === false) { if (to.meta.handleInitialLoad !== true && $store.state.initialLoad === false) {
$store.commit("setInitialLoad", true); $store.commit("setInitialLoad", true);
} }
Vue.nextTick(() => {
document.title = to.meta.title || 'Parsley';
});
}); });
router.addRoutes( router.addRoutes(

View File

@ -2,9 +2,12 @@ import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import api from '../lib/Api'; import api from '../lib/Api';
import { createChannel } from '../lib/ActionCable';
Vue.use(Vuex); Vue.use(Vuex);
let taskChannel = null;
export default new Vuex.Store({ export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production', strict: process.env.NODE_ENV !== 'production',
state: { state: {
@ -99,45 +102,15 @@ export default new Vuex.Store({
state.currentTaskList = list || null; state.currentTaskList = list || null;
}, },
appendTaskItem(state, item) { replaceTaskList(state, list) {
const listId = item.task_list_id; if (state.taskLists) {
const list = state.taskLists.find(l => l.id === listId); const listIdx = state.taskLists.findIndex(l => l.id === list.id);
if (list) { if (listIdx >= 0) {
list.task_items.push(item); state.taskLists.splice(listIdx, 1, list);
} }
}, if (state.currentTaskList && state.currentTaskList.id === list.id) {
state.currentTaskList = list;
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);
} }
}
},
removeTaskItems(state, payload) {
const listId = payload.taskList.id;
const list = state.taskLists.find(l => l.id === listId);
if (list) {
list.task_items = list.task_items.filter(item => {
return payload.taskItems.findIndex(i => i.id === item.id) === -1;
});
}
},
setTaskItemCompletion(state, payload) {
const listId = payload.taskList.id;
const list = state.taskLists.find(l => l.id === listId);
if (list) {
list.task_items.forEach(item => {
if (payload.taskItems.findIndex(i => i.id === item.id) >= 0) {
item.completed = payload.completed;
}
});
} }
} }
}, },
@ -171,7 +144,7 @@ export default new Vuex.Store({
}); });
}, },
refreshTaskLists({commit, state}) { refreshTaskLists({commit, dispatch, state}) {
const cb = function(data) { const cb = function(data) {
commit("setTaskLists", data); commit("setTaskLists", data);
let ctl = null; let ctl = null;
@ -182,6 +155,7 @@ export default new Vuex.Store({
ctl = ctl || data[0] || null; ctl = ctl || data[0] || null;
commit("setCurrentTaskList", ctl); commit("setCurrentTaskList", ctl);
dispatch('ensureTaskListChannel');
}; };
return api.getTaskLists(cb) return api.getTaskLists(cb)
@ -195,6 +169,18 @@ export default new Vuex.Store({
} }
}, },
ensureTaskListChannel({ commit }) {
if (taskChannel === null) {
taskChannel = createChannel(null, "TaskChannel", {
received(data) {
if (data && data.action === 'updated') {
commit('replaceTaskList', data.task_list);
}
}
});
}
},
createTaskList({commit, dispatch}, newList) { createTaskList({commit, dispatch}, newList) {
return api.postTaskList(newList) return api.postTaskList(newList)
.then(data => commit("setCurrentTaskList", data)) .then(data => commit("setCurrentTaskList", data))
@ -210,7 +196,6 @@ export default new Vuex.Store({
return api.postTaskItem(taskItem.task_list_id, taskItem) return api.postTaskItem(taskItem.task_list_id, taskItem)
.then(data => { .then(data => {
commit("appendTaskItem", data);
return data; return data;
}); });
}, },
@ -218,19 +203,16 @@ export default new Vuex.Store({
updateTaskItem({commit}, taskItem) { updateTaskItem({commit}, taskItem) {
return api.patchTaskItem(taskItem.task_list_id, taskItem) return api.patchTaskItem(taskItem.task_list_id, taskItem)
.then(data => { .then(data => {
commit("replaceTaskItem", data);
return data; return data;
}); });
}, },
deleteTaskItems({commit}, payload) { deleteTaskItems({commit}, payload) {
return api.deleteTaskItems(payload.taskList.id, payload.taskItems) return api.deleteTaskItems(payload.taskList.id, payload.taskItems);
.then(() => commit("removeTaskItems", payload));
}, },
completeTaskItems({commit}, payload) { completeTaskItems({commit}, payload) {
return api.completeTaskItems(payload.taskList.id, payload.taskItems, !payload.completed) return api.completeTaskItems(payload.taskList.id, payload.taskItems, !payload.completed);
.then(() => commit("setTaskItemCompletion", payload));
} }
} }
}); });

View File

@ -1,7 +1,7 @@
class TaskItem < ApplicationRecord class TaskItem < ApplicationRecord
include DefaultValues include DefaultValues
belongs_to :task_list, touch: true belongs_to :task_list, touch: true, inverse_of: :task_items
validates :name, presence: true validates :name, presence: true

View File

@ -1,7 +1,7 @@
class TaskList < ApplicationRecord class TaskList < ApplicationRecord
belongs_to :user belongs_to :user
has_many :task_items, dependent: :delete_all has_many :task_items, dependent: :delete_all, inverse_of: :task_list
validates :name, validates :name,
presence: true, presence: true,

View File

@ -51,6 +51,7 @@
</style> </style>
<%= stylesheet_pack_tag 'application' %> <%= stylesheet_pack_tag 'application' %>
<%= action_cable_meta_tag %>
</head> </head>
<body class="loading"> <body class="loading">

View File

@ -53,6 +53,7 @@ module.exports = function(api) {
"loose": true "loose": true
} }
], ],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
[ [
'@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-object-rest-spread',
{ {

View File

@ -11,7 +11,7 @@ require "action_mailer/railtie"
# require "action_mailbox/engine" # require "action_mailbox/engine"
# require "action_text/engine" # require "action_text/engine"
require "action_view/railtie" require "action_view/railtie"
# require "action_cable/engine" require "action_cable/engine"
# require "sprockets/railtie" # require "sprockets/railtie"
require "rails/test_unit/railtie" require "rails/test_unit/railtie"

View File

@ -5,5 +5,10 @@ test:
adapter: async adapter: async
production: production:
adapter: redis adapter: postgresql
url: redis://localhost:6379/1
beta:
adapter: postgresql
docker:
adapter: postgresql

View File

@ -1,9 +1,10 @@
{ {
"dependencies": { "dependencies": {
"@rails/webpacker": "5.3.0", "@rails/actioncable": "^6.1.4",
"@rails/webpacker": "5.4.3",
"@tweenjs/tween.js": "^18.6.4", "@tweenjs/tween.js": "^18.6.4",
"autosize": "^4.0.2", "autosize": "^4.0.2",
"bulma": "^0.8.2", "bulma": "0.9.3",
"cheerio": "^1.0.0-rc.9", "cheerio": "^1.0.0-rc.9",
"css-loader": "^5.2.4", "css-loader": "^5.2.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",

2460
yarn.lock

File diff suppressed because it is too large Load Diff