This commit is contained in:
gsinghpal
2026-05-10 10:25:12 -04:00
parent 6c6a59ceef
commit 6b7b44264a
59 changed files with 2461 additions and 324 deletions

View File

@@ -690,6 +690,86 @@ export class FpSimpleRecipeEditor extends Component {
this._fpResetStepEdit();
}
// -------------------- Instruction images -------------------------------
//
// Recipe authors drop reference photos / screenshots into this list
// while editing a step. Operators see the gallery at runtime in the
// Record Inputs dialog and the step quick-look modal. Backed by
// /fp/simple_recipe/step/image/{add,remove}; mirrors the upload
// affordance available on the tree-editor side.
async onUploadStepImages(stepId, ev) {
const files = Array.from(ev.target.files || []);
if (!files.length) return;
for (const file of files) {
if (!file.type.startsWith("image/")) {
this.notification.add(
_t("%s isn't an image — skipped.").replace("%s", file.name),
{ type: "warning" },
);
continue;
}
// Read as base64 (strip the "data:...;base64," prefix).
// eslint-disable-next-line no-await-in-loop
const datas = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result || "";
resolve(String(result).split(",")[1] || "");
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
// eslint-disable-next-line no-await-in-loop
const result = await rpc("/fp/simple_recipe/step/image/add", {
node_id: stepId,
filename: file.name,
datas: datas,
mimetype: file.type,
});
if (!result.ok) {
this.notification.add(
_t("Could not upload %s.").replace("%s", file.name),
{ type: "danger" },
);
continue;
}
// Append directly to the in-memory step so the gallery
// updates without re-loading the whole recipe tree.
const step = this.state.steps.find((s) => s.id === stepId);
if (step) {
step.instruction_images = [
...(step.instruction_images || []),
result.image,
];
}
}
// Clear the file input so the same file can be uploaded again
// after a remove + re-add cycle.
ev.target.value = "";
this.notification.add(_t("Image(s) attached"), { type: "success" });
}
async onRemoveStepImage(stepId, attachmentId) {
const result = await rpc("/fp/simple_recipe/step/image/remove", {
node_id: stepId,
attachment_id: attachmentId,
});
if (!result.ok) {
this.notification.add(
_t("Could not remove image."),
{ type: "danger" },
);
return;
}
const step = this.state.steps.find((s) => s.id === stepId);
if (step) {
step.instruction_images = (step.instruction_images || []).filter(
(img) => img.id !== attachmentId,
);
}
}
// -------------------- Sub 12d — measurements config --------------------
async onToggleStepCollect(stepId, collect) {

View File

@@ -54,17 +54,61 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
.o_fp_simple_editor_meta {
background: $fp-se-card;
border: 1px solid $fp-se-border;
border-radius: 4px;
padding: 1rem;
border-radius: 6px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
.o_fp_import_row {
display: flex;
align-items: center;
gap: .75rem;
label { font-weight: 500; margin: 0; min-width: 14rem; }
select { flex: 1; max-width: 30rem; }
.o_fp_import_label {
margin: 0;
font-weight: 600;
color: $fp-se-accent;
white-space: nowrap;
display: inline-flex;
align-items: center;
.fa {
color: $fp-se-accent;
opacity: .8;
}
}
// Bootstrap's form-select gives us the chevron + base styling;
// we just tighten the colours to the card tokens so the field
// sits flush in our themed panel instead of fighting it.
.o_fp_import_select {
flex: 1;
max-width: 32rem;
min-height: 2.25rem;
background-color: $fp-se-card;
color: inherit;
border-color: $fp-se-border;
transition: border-color .15s ease, box-shadow .15s ease;
&:hover:not(:focus):not(:disabled) {
border-color: $fp-se-accent;
}
&:focus {
border-color: $fp-se-accent;
box-shadow: 0 0 0 .15rem rgba(46, 125, 107, .18);
outline: none;
}
}
.o_fp_import_btn {
white-space: nowrap;
min-height: 2.25rem;
.fa {
opacity: .9;
}
}
}
}
@@ -492,3 +536,109 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
justify-content: flex-end;
}
// =============================================================================
// Instruction images gallery — recipe-author upload + thumbnail strip in
// the Simple Editor's inline step edit panel. Mirrors what the Record
// Inputs dialog renders at runtime so authors can preview the same way
// the operator will see it.
// =============================================================================
.o_fp_step_images {
.o_fp_step_images_gallery {
display: flex;
flex-wrap: wrap;
gap: .5rem;
margin: .5rem 0;
}
.o_fp_step_image_card {
position: relative;
width: 110px;
background: $fp-se-card;
border: 1px solid $fp-se-border;
border-radius: 6px;
overflow: hidden;
transition: border-color .12s ease, box-shadow .12s ease;
&:hover {
border-color: $fp-se-accent;
box-shadow: 0 2px 6px rgba(0, 0, 0, .12);
}
a {
display: block;
width: 100%;
height: 90px;
cursor: zoom-in;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.o_fp_step_image_remove {
position: absolute;
top: 4px;
right: 4px;
width: 22px;
height: 22px;
padding: 0;
border-radius: 50%;
background: rgba(0, 0, 0, .55);
color: #fff;
border: 0;
cursor: pointer;
opacity: 0;
transition: opacity .12s ease, background-color .12s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: .75rem;
}
.o_fp_step_image_card:hover .o_fp_step_image_remove,
.o_fp_step_image_remove:focus {
opacity: 1;
}
.o_fp_step_image_remove:hover {
background: #c0392b;
}
.o_fp_step_image_caption {
font-size: .7rem;
padding: 4px 6px;
color: $fp-se-muted;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-top: 1px solid $fp-se-border;
}
.o_fp_step_image_uploader {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
margin-top: .25rem;
background: $fp-se-card;
border: 1px dashed $fp-se-border;
border-radius: 6px;
cursor: pointer;
color: $fp-se-accent;
font-weight: 500;
transition: border-color .12s ease, background-color .12s ease;
&:hover {
border-color: $fp-se-accent;
border-style: solid;
background: rgba(46, 125, 107, .06);
}
}
}

View File

@@ -23,8 +23,13 @@
<div class="o_fp_simple_editor_meta" t-if="state.recipe">
<div class="o_fp_import_row">
<label>Import starter from template:</label>
<select t-model="state.selectedTemplate">
<label class="o_fp_import_label" for="fp_import_template_select">
<i class="fa fa-download me-2"/>
Import starter from template
</label>
<select id="fp_import_template_select"
class="form-select o_fp_import_select"
t-model="state.selectedTemplate">
<option value="">— Select template —</option>
<t t-foreach="state.templateOptions" t-as="tpl" t-key="tpl.id">
<option t-att-value="tpl.id">
@@ -32,8 +37,10 @@
</option>
</t>
</select>
<button class="btn btn-primary" t-on-click="onImportTemplate"
<button class="btn btn-primary o_fp_import_btn"
t-on-click="onImportTemplate"
t-att-disabled="!state.selectedTemplate">
<i class="fa fa-plus me-1"/>
Import
</button>
</div>
@@ -193,6 +200,56 @@
</small>
</label>
</div>
<!-- Instruction images — recipe author drops
photos / screenshots / diagrams here.
Operators see the gallery at runtime in
the Record Inputs dialog and the step
quick-look modal. -->
<div class="o_fp_edit_field o_fp_step_images">
<label>
<i class="fa fa-camera me-1"/>
<strong>Instruction Images</strong>
</label>
<p class="o_fp_edit_hint">
Reference photos / screenshots / diagrams shown
to operators while running this step. Drop
multiple images for masking patterns, fixture
orientation, gauge readings, etc.
</p>
<div class="o_fp_step_images_gallery"
t-if="step.instruction_images and step.instruction_images.length">
<t t-foreach="step.instruction_images"
t-as="img" t-key="img.id">
<div class="o_fp_step_image_card">
<a t-att-href="img.url"
target="_blank"
t-att-title="img.name">
<img t-att-src="img.url"
t-att-alt="img.name"/>
</a>
<button type="button"
class="o_fp_step_image_remove"
title="Remove image"
t-on-click="() => this.onRemoveStepImage(step.id, img.id)">
<i class="fa fa-times"/>
</button>
<div class="o_fp_step_image_caption"
t-esc="img.name"/>
</div>
</t>
</div>
<label class="o_fp_step_image_uploader">
<i class="fa fa-plus me-1"/>
Upload images
<input type="file"
accept="image/*"
multiple="multiple"
hidden="hidden"
t-on-change="(ev) => this.onUploadStepImages(step.id, ev)"/>
</label>
</div>
<!-- Sub 12d — Measurements config -->
<div class="o_fp_edit_field o_fp_measurements_config">
<label>