mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-11-04 12:12:48 -05:00
Refactor Job Ideas feature: replace dismiss! with hide! method, update job count logic, and enhance UI components for better visibility and layout
This commit is contained in:
1 parent
7a4bff150e
commit
b22fa68bdf
24 files changed
+144
-115
No files matched your search
@@ -27,7 +27,7 @@ class JobIdeasController < ApplicationController
|
||||
key = params.require(:key)
|
||||
length = params[:length].to_s
|
||||
snoozed = JobIdeaDismissal::SNOOZES.key?(length)
|
||||
snoozed ? JobIdeaDismissal.snooze!(current_member, key, length) : JobIdeaDismissal.dismiss!(current_member, key)
|
||||
snoozed ? JobIdeaDismissal.snooze!(current_member, key, length) : JobIdeaDismissal.hide!(current_member, key)
|
||||
changed([], t("job_ideas.hidden.#{snoozed ? length : 'dismissed'}"))
|
||||
end
|
||||
|
||||
@@ -57,17 +57,23 @@ class JobIdeasController < ApplicationController
|
||||
def changed(added, notice)
|
||||
return redirect_to(job_ideas_path(skip: skipped), notice:) unless request.xhr?
|
||||
|
||||
JobIdeas.forget_count(current_member)
|
||||
@job_ideas = nil
|
||||
load_ideas
|
||||
render json: overlay_json(added)
|
||||
end
|
||||
|
||||
# The member's jobs, as they now are, to show those that were added.
|
||||
def jobs_groups
|
||||
groups = JobsNotepad.new(current_member).groups
|
||||
render_to_string(partial: 'jobs/notepad_groups', formats: [:html], locals: { groups:, in_garden: false })
|
||||
end
|
||||
|
||||
def overlay_json(added)
|
||||
notepad = JobsNotepad.new(current_member)
|
||||
count = @ideas.size + @more
|
||||
{
|
||||
list: render_to_string(partial: 'job_ideas/list', formats: [:html]),
|
||||
groups: render_to_string(partial: 'jobs/notepad_groups', formats: [:html], locals: { groups: notepad.groups, in_garden: false }),
|
||||
groups: (jobs_groups if added.any?),
|
||||
count:, banner: t('job_ideas.banner', count:), added_label: t('job_ideas.added_to_jobs', count: added.size),
|
||||
added: added.map { |job| { id: job.id, name: job.name, due: (l(job.due_date, format: '%-d %b') if job.due_date) } }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module JobIdeasHelper
|
||||
# How many ideas a member has, for the count beside the link to them. It looks
|
||||
# at other people's jobs, so it is kept for a few minutes, and changes at once
|
||||
# when they hide an idea.
|
||||
def job_ideas_count(member)
|
||||
hidden = member.job_idea_dismissals.maximum(:updated_at)
|
||||
changed = member.jobs.maximum(:updated_at)
|
||||
Rails.cache.fetch(['job_ideas_count', member.id, hidden, changed], expires_in: 10.minutes) { JobIdeas.new(member).ideas.size }
|
||||
end
|
||||
|
||||
# What an idea would add, as the member would read it.
|
||||
def job_idea_title(idea)
|
||||
case idea.kind
|
||||
@@ -23,7 +14,7 @@ module JobIdeasHelper
|
||||
def job_idea_reason(idea)
|
||||
day = idea.on && l(idea.on, format: '%-d %b')
|
||||
case idea.kind
|
||||
when :template then t('job_ideas.reason.template', count: idea.job_count, steps: job_idea_steps(idea), crop: idea.crop.name, day:)
|
||||
when :template then t('job_ideas.reason.template', count: idea.schedule.size, steps: job_idea_steps(idea), crop: idea.crop.name, day:)
|
||||
when :past_job, :past_planting then t("job_ideas.reason.#{idea.kind}", count: idea.years, day:)
|
||||
when :harvest then t("job_ideas.reason.harvest_#{idea.period}", day:)
|
||||
else community_reason(idea, day)
|
||||
|
||||
@@ -84,7 +84,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
// arrive.
|
||||
const showJobs = function (data) {
|
||||
const groups = document.getElementById('notepad-groups');
|
||||
if (!groups) return;
|
||||
if (!groups || !data.groups) return;
|
||||
const added = new Set(data.added.map(function (job) { return String(job.id); }));
|
||||
groups.innerHTML = data.groups;
|
||||
groups.querySelectorAll('.notepad-job').forEach(function (row) {
|
||||
@@ -92,25 +92,39 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
};
|
||||
|
||||
const choose = function (form) {
|
||||
const row = form.closest('.idea');
|
||||
if (row) row.classList.add('idea-working');
|
||||
// Sends the choice. Rejects only if it could not be sent, or the answer was no
|
||||
// good: then it is safe to send it again the ordinary way.
|
||||
const send = function (form) {
|
||||
return request(form.action, {method: 'POST', body: new FormData(form), headers: {Accept: 'application/json'}})
|
||||
.then(ok)
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (data) {
|
||||
.then(function (response) { return response.json(); });
|
||||
};
|
||||
|
||||
// Shows what the server did. If that goes wrong the choice was made all the
|
||||
// same, so the page is loaded again to show how things stand.
|
||||
const show = function (form, data) {
|
||||
const row = form.closest('.idea');
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
showCount(data);
|
||||
showJobs(data);
|
||||
if (data.added.length && row) {
|
||||
row.replaceChildren(addedBlock(data));
|
||||
row.classList.remove('idea-working');
|
||||
row.classList.add('idea-confirmed');
|
||||
return wait(Math.min(2600, 1100 + data.added.length * 140)).then(function () { return data; });
|
||||
return wait(Math.min(2600, 1100 + data.added.length * 140));
|
||||
}
|
||||
if (row) row.classList.add('idea-leaving');
|
||||
return wait(350).then(function () { return data; });
|
||||
return wait(350);
|
||||
})
|
||||
.then(function (data) { showList(data.list); });
|
||||
.then(function () { showList(data.list); })
|
||||
.catch(function () { window.location.reload(); });
|
||||
};
|
||||
|
||||
const choose = function (form) {
|
||||
const row = form.closest('.idea');
|
||||
if (row) row.classList.add('idea-working');
|
||||
return send(form).then(function (data) { return show(form, data); }, function () { form.submit(); });
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-job-ideas-open]').forEach(function (link) {
|
||||
@@ -132,6 +146,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = event.target.closest('.idea-form');
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
choose(form).catch(function () { form.submit(); });
|
||||
choose(form);
|
||||
});
|
||||
});
|
||||
@@ -146,6 +146,12 @@ describe('choosing an idea', () => {
|
||||
expect(document.querySelector('.notepad-ideas').hidden).toBe(true);
|
||||
});
|
||||
|
||||
it('does not send the choice again once the server has answered', async () => {
|
||||
await choose();
|
||||
const form = body().querySelector('.idea-form');
|
||||
expect(form === null || form.submit === HTMLFormElement.prototype.submit).toBe(true);
|
||||
});
|
||||
|
||||
it('sends the choice the ordinary way if it could not be sent', async () => {
|
||||
fetchMock.mockResolvedValueOnce(html(idea('a', 'Snip'))).mockRejectedValueOnce(new Error('offline'));
|
||||
await start();
|
||||
|
||||
@@ -20,10 +20,6 @@ class JobIdeaDismissal < ApplicationRecord
|
||||
dismissal
|
||||
end
|
||||
|
||||
def self.dismiss!(member, key)
|
||||
hide!(member, key)
|
||||
end
|
||||
|
||||
# For one of SNOOZES, named.
|
||||
def self.snooze!(member, key, length)
|
||||
hide!(member, key, SNOOZES.fetch(length))
|
||||
|
||||
@@ -8,19 +8,32 @@ class JobIdeas
|
||||
# How many ideas are suggested at a time.
|
||||
SHOWN = 5
|
||||
|
||||
# kind is where the idea came from. job_count is how many jobs a template would
|
||||
# add, and schedule those jobs, with their days; on is the latest day it was done or planted at this time of year, and
|
||||
# years how many years it was. From other gardeners, gardeners is how many did
|
||||
# it, of how many grow the crop, now or in earlier years, as period says. For
|
||||
# a harvest, on is the day it was last harvested from, or if never, the day it
|
||||
# was expected to be ready, as period says.
|
||||
Idea = Struct.new(:kind, :key, :name, :crop, :planting, :template, :schedule, :job_count, :on, :years, :gardeners, :of, :period,
|
||||
# Where an idea came from is its kind. The rest is what the kind has to say:
|
||||
# - planting and template: the planting it is for; a template idea also has the
|
||||
# template and its schedule, the jobs it would add with their days
|
||||
# - on: the latest day it was done or planted at this time of year, and years how
|
||||
# many years; for a harvest, the day it was last harvested from or, if never,
|
||||
# expected to be ready, as period says
|
||||
# - gardeners: from other gardeners, how many did it, of how many grow the crop,
|
||||
# now or in earlier years, as period says
|
||||
Idea = Struct.new(:kind, :key, :name, :crop, :planting, :template, :schedule, :on, :years, :gardeners, :of, :period,
|
||||
keyword_init: true)
|
||||
|
||||
# Sources a member can switch off, by name, and the ones each name stands for.
|
||||
# Templates and harvests are the member's own garden, so they are always on.
|
||||
SWITCHES = { 'mine' => %i(past_jobs past_plantings), 'community' => %i(community), 'nearby' => %i(nearby) }.freeze
|
||||
|
||||
# How many ideas a member has, for the box over their jobs. It looks at other
|
||||
# people's jobs, so it is kept for a few minutes, until the member adds or hides
|
||||
# an idea.
|
||||
def self.count_for(member)
|
||||
Rails.cache.fetch(['job_ideas_count', member.id], expires_in: 10.minutes) { new(member).ideas.size }
|
||||
end
|
||||
|
||||
def self.forget_count(member)
|
||||
Rails.cache.delete(['job_ideas_count', member.id])
|
||||
end
|
||||
|
||||
def initialize(member, today: Time.zone.today, skip: [])
|
||||
@member = member
|
||||
@today = today
|
||||
@@ -56,6 +69,6 @@ class JobIdeas
|
||||
end
|
||||
|
||||
def hidden_keys
|
||||
@hidden_keys ||= @member.job_idea_dismissals.hiding.pluck(:key)
|
||||
@hidden_keys ||= @member.job_idea_dismissals.hiding.pluck(:key).to_set
|
||||
end
|
||||
end
|
||||
@@ -27,7 +27,7 @@ class JobIdeas
|
||||
# template would come back for the next planting of the crop.
|
||||
def add_template
|
||||
jobs = ApplyCropJobTemplate.new(@idea.template, @idea.planting, today: @today).call
|
||||
JobIdeaDismissal.dismiss!(@member, @idea.key)
|
||||
JobIdeaDismissal.hide!(@member, @idea.key)
|
||||
jobs
|
||||
end
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
# same time of year; nobody if the member has not said where they are.
|
||||
class JobIdeas
|
||||
class Community < OthersJobs
|
||||
include SeasonsHelper
|
||||
|
||||
private
|
||||
|
||||
def owner_ids
|
||||
return [] unless @member.latitude
|
||||
|
||||
Member.confirmed.located.where(@member.latitude.negative? ? 'latitude < 0' : 'latitude >= 0').pluck(:id)
|
||||
@owner_ids ||= case hemisphere(@member.latitude)
|
||||
when :southern then Member.confirmed.located.where('latitude < 0').pluck(:id)
|
||||
when :northern then Member.confirmed.located.where('latitude >= 0').pluck(:id)
|
||||
else []
|
||||
end
|
||||
end
|
||||
|
||||
def crop_ids
|
||||
@@ -21,10 +25,8 @@ class JobIdeas
|
||||
:community
|
||||
end
|
||||
|
||||
def growing(crop)
|
||||
return unless crop
|
||||
|
||||
growers[crop.id] || 0
|
||||
def growing(crop_id)
|
||||
growers[crop_id] || 0 if crop_id
|
||||
end
|
||||
|
||||
def growers
|
||||
|
||||
@@ -14,13 +14,15 @@ class JobIdeas
|
||||
end
|
||||
|
||||
def ideas
|
||||
plantings.filter_map { |planting| idea_for(planting) }
|
||||
found = plantings.to_a
|
||||
@last_harvests = ::Harvest.where(planting_id: found.map(&:id)).group(:planting_id).maximum(:harvested_at)
|
||||
found.filter_map { |planting| idea_for(planting) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def plantings
|
||||
@member.plantings.active.annual.where.not(planted_at: nil).preload(:crop, :harvests)
|
||||
@member.plantings.active.annual.where.not(planted_at: nil).preload(:crop)
|
||||
end
|
||||
|
||||
def idea_for(planting)
|
||||
@@ -42,7 +44,7 @@ class JobIdeas
|
||||
end
|
||||
|
||||
def last_harvest(planting)
|
||||
planting.harvests.filter_map(&:harvested_at).max&.to_date
|
||||
@last_harvests[planting.id]&.to_date
|
||||
end
|
||||
|
||||
def expected_on(planting)
|
||||
|
||||
@@ -9,9 +9,11 @@ class JobIdeas
|
||||
private
|
||||
|
||||
def owner_ids
|
||||
return [] unless @member.latitude && @member.longitude
|
||||
|
||||
Member.confirmed.located.near([@member.latitude, @member.longitude], RADIUS_KM, units: :km).reorder(nil).pluck(:id)
|
||||
@owner_ids ||= if @member.latitude && @member.longitude
|
||||
Member.confirmed.located.near([@member.latitude, @member.longitude], RADIUS_KM, units: :km).reorder(nil).pluck(:id)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def kind
|
||||
|
||||
@@ -13,7 +13,6 @@ class JobIdeas
|
||||
MIN_GARDENERS = 3
|
||||
MAX_IDEAS = 5
|
||||
CROP_ID = "COALESCE(CASE WHEN jobs.jobbable_type = 'Crop' THEN jobs.jobbable_id END, plantings.crop_id, seeds.crop_id)"
|
||||
WITHIN = '(jobs.finished_at BETWEEN ? AND ? OR (jobs.finished_at IS NULL AND jobs.due_date BETWEEN ? AND ?))'
|
||||
Row = Struct.new(:name, :owner_id, :crop_id, :on)
|
||||
|
||||
def initialize(member, today)
|
||||
@@ -24,8 +23,9 @@ class JobIdeas
|
||||
def ideas
|
||||
return [] if owner_ids&.empty? || crop_ids&.empty?
|
||||
|
||||
crops = Crop.where(id: grouped.keys.filter_map(&:last)).index_by(&:id)
|
||||
grouped.filter_map { |(_name, crop_id), rows| idea_for(rows, crops[crop_id]) }.max_by(MAX_IDEAS, &:gardeners)
|
||||
found = candidates.max_by(MAX_IDEAS) { |candidate| candidate[:gardeners] }
|
||||
crops = Crop.where(id: found.filter_map { |candidate| candidate[:crop_id] }).index_by(&:id)
|
||||
found.map { |candidate| idea_for(candidate, crops[candidate[:crop_id]]) }
|
||||
end
|
||||
|
||||
private
|
||||
@@ -46,57 +46,43 @@ class JobIdeas
|
||||
|
||||
# How many gardeners grow the crop, to say how many of them did it. Only
|
||||
# worth saying when the jobs are limited to those growing the crop.
|
||||
def growing(_crop)
|
||||
def growing(_crop_id)
|
||||
nil
|
||||
end
|
||||
|
||||
def idea_for(rows, crop)
|
||||
return if open_names.include?(rows.first.name.downcase)
|
||||
# What each job done by enough gardeners comes to, before the crops are loaded.
|
||||
def candidates
|
||||
rows.group_by { |row| [row.name.downcase, row.crop_id] }.filter_map do |(name, crop_id), group|
|
||||
next if open_names.include?(name)
|
||||
|
||||
now, earlier = rows.partition { |row| row.on > @today - WINDOW }
|
||||
owners = [now, earlier].map { |group| group.map(&:owner_id).uniq.size }
|
||||
period, gardeners = owners.first >= MIN_GARDENERS ? [:now, owners.first] : [:earlier, owners.last]
|
||||
return if gardeners < MIN_GARDENERS
|
||||
now, earlier = group.partition { |row| row.on > @today - WINDOW }
|
||||
owners = [now, earlier].map { |found| found.map(&:owner_id).uniq.size }
|
||||
period, gardeners = owners.first >= MIN_GARDENERS ? [:now, owners.first] : [:earlier, owners.last]
|
||||
next if gardeners < MIN_GARDENERS
|
||||
|
||||
Idea.new(kind:, key: "#{kind}:#{rows.first.name.downcase}:#{crop&.id}", name: rows.first.name, crop:, gardeners:,
|
||||
period:, of: growing(crop), on: rows.map(&:on).max)
|
||||
{ name: group.first.name, crop_id:, period:, gardeners:, on: group.map(&:on).max }
|
||||
end
|
||||
end
|
||||
|
||||
def grouped
|
||||
@grouped ||= rows.group_by { |row| [row.name.downcase, row.crop_id] }
|
||||
def idea_for(candidate, crop)
|
||||
Idea.new(kind:, key: "#{kind}:#{candidate[:name].downcase}:#{crop&.id}", name: candidate[:name], crop:,
|
||||
gardeners: candidate[:gardeners], period: candidate[:period], of: growing(crop&.id), on: candidate[:on])
|
||||
end
|
||||
|
||||
def rows
|
||||
jobs = Job.where(finished: true, crop_job_step_id: nil).where.not(owner_id: left_out_ids)
|
||||
jobs = jobs.where(owner_id: owner_ids) if owner_ids
|
||||
jobs = jobs.where("#{CROP_ID} IN (?)", crop_ids) if crop_ids
|
||||
jobs.joins("LEFT JOIN plantings ON jobs.jobbable_type = 'Planting' AND plantings.id = jobs.jobbable_id")
|
||||
.joins("LEFT JOIN seeds ON jobs.jobbable_type = 'Seed' AND seeds.id = jobs.jobbable_id")
|
||||
.where(in_windows, *window_values)
|
||||
finished_in(jobs.joins("LEFT JOIN plantings ON jobs.jobbable_type = 'Planting' AND plantings.id = jobs.jobbable_id")
|
||||
.joins("LEFT JOIN seeds ON jobs.jobbable_type = 'Seed' AND seeds.id = jobs.jobbable_id"),
|
||||
[(@today - WINDOW)..@today] + past_windows)
|
||||
.pluck('jobs.name', 'jobs.owner_id', 'jobs.finished_at', 'jobs.due_date', Arel.sql(CROP_ID))
|
||||
.map { |name, owner_id, finished_at, due_date, crop_id| Row.new(name, owner_id, crop_id, finished_at&.to_date || due_date) }
|
||||
end
|
||||
|
||||
# Now, and the same fortnight in each earlier year.
|
||||
def windows
|
||||
[(@today - WINDOW)..@today] + past_windows
|
||||
end
|
||||
|
||||
def in_windows
|
||||
windows.map { WITHIN }.join(' OR ')
|
||||
end
|
||||
|
||||
def window_values
|
||||
windows.flat_map { |window| [window.first.beginning_of_day, window.last.end_of_day, window.first, window.last] }
|
||||
end
|
||||
|
||||
# Themselves, and the people they have blocked.
|
||||
def left_out_ids
|
||||
[@member.id] + @member.blocks.pluck(:blocked_id)
|
||||
end
|
||||
|
||||
def open_names
|
||||
@open_names ||= @member.jobs.active.pluck(:name).map(&:downcase)
|
||||
@left_out_ids ||= [@member.id] + @member.blocks.pluck(:blocked_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -27,18 +27,15 @@ class JobIdeas
|
||||
# Each job with the day it was done. A job finished before the time was
|
||||
# recorded was done on the day it was due.
|
||||
def done_jobs
|
||||
jobs = past_windows.map { |window| finished_in(window) }.reduce(:or)
|
||||
jobs.includes(:jobbable).map { |job| [job, job.finished_at&.to_date || job.due_date] }
|
||||
jobs = finished_in(@member.jobs.where(finished: true, crop_job_step_id: nil), past_windows).includes(:jobbable).to_a
|
||||
preload_crops(jobs)
|
||||
jobs.map { |job| [job, job.finished_at&.to_date || job.due_date] }
|
||||
end
|
||||
|
||||
def finished_in(window)
|
||||
@member.jobs.where(finished: true, crop_job_step_id: nil)
|
||||
.where('jobs.finished_at BETWEEN ? AND ? OR (jobs.finished_at IS NULL AND jobs.due_date BETWEEN ? AND ?)',
|
||||
window.first.beginning_of_day, window.last.end_of_day, window.first, window.last)
|
||||
end
|
||||
|
||||
def open_names
|
||||
@open_names ||= @member.jobs.active.pluck(:name).map(&:downcase)
|
||||
# The crop of every planting and seed a job is about, in one go.
|
||||
def preload_crops(jobs)
|
||||
about = jobs.filter_map(&:jobbable).grep_v(Crop)
|
||||
ActiveRecord::Associations::Preloader.new(records: about, associations: :crop).call
|
||||
end
|
||||
|
||||
def key_for(job)
|
||||
|
||||
@@ -17,8 +17,9 @@ class JobIdeas
|
||||
next if planted_since.include?(crop_id) || to_plant.include?(crop_id)
|
||||
|
||||
latest = plantings.max_by(&:planted_at)
|
||||
years = plantings.map { |planting| planting.planted_at.year }.uniq.size
|
||||
Idea.new(kind: :past_planting, key: "past_planting:#{crop_id}", name: latest.crop.name, crop: latest.crop,
|
||||
planting: latest, on: latest.planted_at, years: plantings.map { |planting| planting.planted_at.year }.uniq.size)
|
||||
on: latest.planted_at, years:)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
|
||||
class JobIdeas
|
||||
# The time of year a source looks back to: the fortnight starting on today's day
|
||||
# in each of the years before, as far back as MAX_YEARS.
|
||||
# in each of the years before, as far back as MAX_YEARS. Also what the sources
|
||||
# share about jobs, which are finished in a window and which the member already
|
||||
# has open.
|
||||
module PastYears
|
||||
WINDOW = 14.days
|
||||
MAX_YEARS = 5
|
||||
# Done in the window by the time it was crossed off or, if that was not
|
||||
# recorded, the day it was due.
|
||||
FINISHED_IN = '(jobs.finished_at BETWEEN ? AND ? OR (jobs.finished_at IS NULL AND jobs.due_date BETWEEN ? AND ?))'
|
||||
|
||||
private
|
||||
|
||||
@@ -13,5 +18,17 @@ class JobIdeas
|
||||
def past_windows
|
||||
(1..MAX_YEARS).map { |years| (@today - years.years)..(@today - years.years + WINDOW) }
|
||||
end
|
||||
|
||||
# The finished jobs in `jobs` that were done in any of the windows.
|
||||
def finished_in(jobs, windows)
|
||||
windows.map do |window|
|
||||
jobs.where(FINISHED_IN, window.first.beginning_of_day, window.last.end_of_day, window.first, window.last)
|
||||
end.reduce(:or)
|
||||
end
|
||||
|
||||
# The names of the jobs the member has open, lower case.
|
||||
def open_names
|
||||
@open_names ||= @member.jobs.active.pluck(:name).to_set(&:downcase)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -15,7 +15,7 @@ class JobIdeas
|
||||
by_template.map do |template, found|
|
||||
planting, entries = found.max_by { |planting, _entries| [planting.planted_at, planting.id] }
|
||||
Idea.new(kind: :template, key: "template:#{template.id}", name: template.name, crop: template.crop, planting:, template:,
|
||||
schedule: entries, job_count: entries.size, on: planting.planted_at.to_date)
|
||||
schedule: entries, on: planting.planted_at.to_date)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
%span.idea-preview-day= l(entry.date, format: '%-d %b')
|
||||
= entry.step.name
|
||||
.idea-actions
|
||||
- add = idea.kind == :template ? t('job_ideas.add_all', count: idea.job_count) : t('job_ideas.add')
|
||||
- add = idea.kind == :template ? t('job_ideas.add_all', count: idea.schedule.size) : t('job_ideas.add')
|
||||
= button_to add, job_ideas_path(skip:), params: { key: idea.key }, class: 'idea-add', form_class: 'idea-form'
|
||||
.idea-later
|
||||
- dismiss = { params: { key: idea.key }, class: 'idea-link', form_class: 'idea-form' }
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
.col-xl-6.mb-4.order-first.order-xl-last
|
||||
.notepad.job-ideas
|
||||
.notepad-title
|
||||
%h2= t('job_ideas.card_title')
|
||||
%h2= t('job_ideas.title')
|
||||
= render 'job_ideas/list'
|
||||
.col-xl-6.mb-4
|
||||
= render 'jobs/notepad', owner: current_member, groups: @job_groups, done_this_week: @done_this_week,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-# The ideas over a member's jobs: the yellow box that says there are some, if there
|
||||
-# are, and the overlay it opens.
|
||||
- count = JobIdeas.count_for(owner)
|
||||
= render 'jobs/ideas_banner', count: count if count.positive?
|
||||
= render 'jobs/ideas_modal'
|
||||
@@ -5,7 +5,7 @@
|
||||
.modal-dialog.modal-dialog-scrollable.modal-lg
|
||||
.modal-content
|
||||
.modal-header
|
||||
%h2.modal-title#job-ideas-title= t('job_ideas.card_title')
|
||||
%h2.modal-title#job-ideas-title= t('job_ideas.title')
|
||||
%button.btn-close{ type: 'button', data: { bs_dismiss: 'modal' }, 'aria-label': t('job_ideas.done') }
|
||||
.modal-body.p-0
|
||||
.notepad.job-ideas#job-ideas-body{ 'aria-live': 'polite' }
|
||||
|
||||
@@ -25,10 +25,7 @@
|
||||
%span.notepad-add-saving-text
|
||||
%p.notepad-add-failed.text-danger.mx-3{ id: 'notepad-add-failed', role: 'alert', hidden: true }= t('jobs.pad.add_failed')
|
||||
%p.notepad-note= t('jobs.pad.public_note') + ' ' + t('jobs.pad.link_hint')
|
||||
- banner = !garden && local_assigns.fetch(:ideas_banner, true)
|
||||
- ideas = job_ideas_count(owner) if banner
|
||||
= render 'jobs/ideas_banner', count: ideas if ideas&.positive?
|
||||
= render 'jobs/ideas_modal' if banner
|
||||
= render 'jobs/ideas', owner: owner unless garden || !local_assigns.fetch(:ideas_banner, true)
|
||||
|
||||
#notepad-groups{ data: { celebrations: job_celebrations.to_json, garden_id: garden&.id } }= render 'jobs/notepad_groups', groups:, in_garden: garden.present?
|
||||
.notepad-snail
|
||||
|
||||
@@ -665,7 +665,6 @@ en:
|
||||
unknown: Unknown
|
||||
job_ideas:
|
||||
title: Ideas for now
|
||||
card_title: Ideas for now
|
||||
banner:
|
||||
one: We have 1 suggested job for you this week
|
||||
other: We have %{count} suggested jobs for you this week
|
||||
@@ -691,7 +690,6 @@ en:
|
||||
nearby: Near you
|
||||
nearby_note: "%{place} and %{km} km around"
|
||||
none: Nothing to suggest right now. Check back as the season changes.
|
||||
back: Back to your jobs
|
||||
add: "+ Add"
|
||||
add_all: Add all %{count}
|
||||
preview:
|
||||
|
||||
@@ -24,8 +24,8 @@ describe JobIdeaDismissal do
|
||||
end
|
||||
end
|
||||
|
||||
describe '.dismiss!' do
|
||||
before { described_class.dismiss!(member, key) }
|
||||
describe '.hide!' do
|
||||
before { described_class.hide!(member, key) }
|
||||
|
||||
it 'hides the idea' do
|
||||
expect(hidden_keys).to eq [key]
|
||||
@@ -61,7 +61,7 @@ describe JobIdeaDismissal do
|
||||
end
|
||||
|
||||
it 'does not keep two for the same idea' do
|
||||
described_class.dismiss!(member, key)
|
||||
described_class.hide!(member, key)
|
||||
expect(member.job_idea_dismissals.count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,10 +19,6 @@ describe JobIdeas::PastPlantings do
|
||||
expect(ideas.first.on).to eq planted_at
|
||||
end
|
||||
|
||||
it 'is about the planting it was last done' do
|
||||
expect(ideas.first.planting).to eq planting
|
||||
end
|
||||
|
||||
it 'keys the idea by crop' do
|
||||
expect(ideas.first.key).to eq "past_planting:#{crop.id}"
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ describe JobIdeas::Templates do
|
||||
end
|
||||
|
||||
it 'counts the jobs it would add' do
|
||||
expect(ideas.first.job_count).to eq 2
|
||||
expect(ideas.first.schedule.size).to eq 2
|
||||
end
|
||||
|
||||
it 'keys the idea by template' do
|
||||
@@ -52,7 +52,7 @@ describe JobIdeas::Templates do
|
||||
end
|
||||
|
||||
it 'counts the jobs for that planting only' do
|
||||
expect(ideas.first.job_count).to eq 2
|
||||
expect(ideas.first.schedule.size).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in new issue
Block a user