Class: Ecoportal::API::GraphQL::Base::Page::DataField::ImageGallery

Inherits:
DataField
  • Object
show all
Defined in:
lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb

Overview

Image gallery field.

★ FIXED 2026-09 (previously broken -- see spec/.../image_gallery_characterization_spec.rb git history for the prior "WRITE PATH BROKEN" documentation of this defect). Confirmed against the LIVE ecoPortal server source (read-only, /tmp/work/ecoPortal) that a gallery image is genuinely NOT a FileContainer:

* `app/graphql/types/fill_in_page/inputs/data_fields/image_gallery_input.rb:5-11` --
`ImageGalleryInput` has NO `fileContainerIds`; it takes `images: [ImageInput]`.
* `.../image_galleries/image_input.rb:6-15` -- `ImageInput` takes `sourceId` (a
`TempImage` id, from the SEPARATE `uploadImage` mutation -- see
`Mutation::Image::Upload` / `FileUpload::Client#upload_image`), never
`fileContainerId`.
* `app/services/new_ep/pages/assign_attributes_service.rb:152-176`
(`set_image_gallery_attrs`) -- full-replace-by-omission, the SAME trap as
`FileField`: any current image whose `id` is not echoed back in the write is
destroyed (`format_removed_ids`).
* `app/graphql/types/pages/data_fields/image_galleries/image_type.rb` -- `fileName`/
`fileSize` are NULLABLE even on a successful upload -- a documented upstream bug
("images are successfully uploaded but the file name and size are not set in the
database", per the live schema's own field comment). Any comparison built on these
two fields must treat a nil on either side as "not a confident match", never a
positive one.

READ: images { id weight downloadUrl caption fileName fileSize uploadId } (imageGalleryField fragment, fragment/pages/common_page_union.rb -- id/weight were ADDED alongside this fix; the fragment previously omitted both, which is why no correct reader was possible before now).

WRITE: ImageGalleryInput { id images: [ImageInput] }; ImageInput { id weight sourceId caption fileName sensitiveContent inaccurateDescription inaccurateExtractedText }. as_input sends every KEPT image back with its FULL ImageInput field set (#kept_image_input) -- NOT a bare {id:} -- and every NEW one as {sourceId:, weight:, fileName:} -- see #add_source_images, the merge-safe writer (mirrors FileField#file_container_ids='s kept-vs-new split).

★ CORRECTED 2026-09 (pre-release, MR !278 unreleased): the bare {id:} echo for a kept image, above, silently dropped every OTHER attribute on that image on the NEXT write server-side -- assign_attributes_service.rb#set_image_gallery_attrs applies images as a FULL REPLACE (see the class header above), so a bare {id:} is indistinguishable from "clear caption/sensitiveContent/etc back to their defaults" for that image, not "leave it as it is". Matches the org-side reference implementation (a downstream script repo, commit 482db9c) and the live schema's own ImageInput argument list -- the shipped ep-api-collections sample only demonstrates the NEW-image shape (no existing images to keep), so this shape is pinned to server source + that proven reference, not a captured kept-echo payload (see EVIDENCE PRECEDENCE note on #kept_image_input).

images is deliberately a PLAIN doc reader, not passarray -- same reasoning as FileField#items (see that class's header): materialising an ArrayModel writes images: [] into the doc of a field read without $content, and a phantom-dirty empty images here doesn't always no-op (only the empty-vs-missing-key case is neutralised by Diffable::LeafDiffService's array-diff fix, MR !106) -- it can still synthesise an unwanted write for a non-empty phantom read. A plain reader has no such hazard.

Instance Method Summary collapse

Instance Method Details

#add_source_images(sources) ⇒ Array<Hash>

Merge-safe write: adds NEW images (each referenced by source_id, the TempImage id from FileUpload::Client#upload_image) while KEEPING every image already loaded on this field. images on ImageGalleryInput is a FULL REPLACE server-side -- any current image whose id is not echoed back is destroyed -- so this always keeps every currently-loaded image's FULL doc entry (untouched -- #images_input picks the ImageInput-writable subset out of it later; kept HERE in full so that subset still has every field to read, rather than collapsing to a bare {'id' => ...} that would throw the other attributes away before #as_input ever runs) and appends the new ones after it, at sequential weights starting at #next_weight. Marks the field dirty (same underlying mechanism as FileField#file_container_ids= -- a raw doc mutation, picked up by the generic diff regardless of the passarray/plain-reader choice above).

Parameters:

  • sources (Array<Hash>) —

    one entry per NEW image. Accepts either key style: {source_id:, file_name:} or {'sourceId' =>, 'fileName' =>}. weight is assigned here, not read from the input -- the caller does not need to compute it.

Returns:

  • (Array<Hash>) —

    the full resulting images doc (kept + added), same value now readable via #images.



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb', line 96

def add_source_images(sources)
  kept  = images.select { |image| image.is_a?(Hash) && image['id'] }
  start = next_weight
  added = Array(sources).each_with_index.map do |source, index|
    {
      'sourceId' => fetch_key(source, :source_id, 'sourceId'),
      'weight'   => start + index,
      'fileName' => fetch_key(source, :file_name, 'fileName')
    }.compact
  end
  doc['images'] = kept + added
end

#as_input ⇒ Object



109
110
111
112
113
# File 'lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb', line 109

def as_input
  return nil unless dirty?

  {imageGallery: {id: id, images: images_input}}
end

#image_ids ⇒ Array<String>

Returns every image's own item id (its server-assigned id, NOT the uploadId/TempImage id it was created from).

Returns:

  • (Array<String>) —

    every image's own item id (its server-assigned id, NOT the uploadId/TempImage id it was created from).



64
65
66
# File 'lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb', line 64

def image_ids
  images.filter_map { |image| image['id'] if image.is_a?(Hash) }
end

#images ⇒ Object

each: { 'id' =>, 'weight' =>, 'downloadUrl' =>, 'caption' =>, 'fileName' =>, 'fileSize' =>, 'uploadId' => }



58
59
60
# File 'lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb', line 58

def images
  Array(doc['images'])
end

#next_weight ⇒ Integer

Returns one past the highest weight currently on the field (0 if the field is empty). There is no server-side auto-increment for weight (a plain Integer field, default 0) -- callers that want sequential ordering for several new images in one write should still call this ONCE and increment locally (#add_source_images already does).

Returns:

  • (Integer) —

    one past the highest weight currently on the field (0 if the field is empty). There is no server-side auto-increment for weight (a plain Integer field, default 0) -- callers that want sequential ordering for several new images in one write should still call this ONCE and increment locally (#add_source_images already does).



73
74
75
76
77
# File 'lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb', line 73

def next_weight
  return 0 if images.empty?

  images.map { |image| image.is_a?(Hash) ? image['weight'].to_i : 0 }.max + 1
end