changes
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user