• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geonetwork / geonetwork-ui / 14086520528

26 Mar 2025 02:58PM UTC coverage: 83.969% (+2.1%) from 81.86%
14086520528

Pull #1187

github

web-flow
Merge 4296dd1ef into 4de378079
Pull Request #1187: [Editor] Image upload component UI error state

1571 of 2117 branches covered (74.21%)

Branch coverage included in aggregate %.

5 of 13 new or added lines in 1 file covered. (38.46%)

1 existing line in 1 file now uncovered.

5034 of 5749 relevant lines covered (87.56%)

10.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.01
/libs/ui/elements/src/lib/image-input/image-input.component.ts
1
import { CommonModule } from '@angular/common'
1✔
2
import { HttpClient } from '@angular/common/http'
1✔
3
import {
1✔
4
  ChangeDetectionStrategy,
5
  ChangeDetectorRef,
6
  Component,
7
  EventEmitter,
8
  Input,
9
  Output,
10
} from '@angular/core'
11
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'
1✔
12
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
1✔
13
import {
1✔
14
  ButtonComponent,
15
  FilesDropDirective,
16
  TextInputComponent,
17
  UrlInputComponent,
18
} from '@geonetwork-ui/ui/inputs'
19
import { downgradeImage, megabytesToBytes } from '@geonetwork-ui/util/shared'
1✔
20
import {
1✔
21
  NgIconComponent,
22
  provideIcons,
23
  provideNgIconsConfig,
24
} from '@ng-icons/core'
25
import {
1✔
26
  iconoirBin,
27
  iconoirFramePlusIn,
28
  iconoirLink,
29
  iconoirMediaImage,
30
  iconoirMediaImageXmark,
31
  iconoirPlus,
32
} from '@ng-icons/iconoir'
33
import { TranslateModule } from '@ngx-translate/core'
1✔
34
import { firstValueFrom } from 'rxjs'
1✔
35
import { ImageOverlayPreviewComponent } from '../image-overlay-preview/image-overlay-preview.component'
1✔
36

37
@Component({
38
  selector: 'gn-ui-image-input',
39
  templateUrl: './image-input.component.html',
40
  styleUrls: ['./image-input.component.css'],
41
  changeDetection: ChangeDetectionStrategy.OnPush,
42
  standalone: true,
43
  imports: [
44
    CommonModule,
45
    ButtonComponent,
46
    FilesDropDirective,
47
    MatProgressSpinnerModule,
48
    TranslateModule,
49
    UrlInputComponent,
50
    TextInputComponent,
51
    NgIconComponent,
52
    ImageOverlayPreviewComponent,
53
  ],
54
  providers: [
55
    provideIcons({
56
      iconoirMediaImage,
57
      iconoirFramePlusIn,
58
      iconoirMediaImageXmark,
59
      iconoirBin,
60
      iconoirPlus,
61
      iconoirLink,
62
    }),
63
    provideNgIconsConfig({
64
      size: '1.5rem',
65
    }),
66
  ],
67
})
68
export class ImageInputComponent {
1✔
69
  @Input() maxSizeMB: number
70
  @Input() previewUrl?: string
71
  @Input() altText?: string
72
  @Input() uploadProgress?: number
73
  @Input() uploadError?: boolean
74
  @Input() disabled?: boolean = false
6✔
75
  @Output() fileChange: EventEmitter<File> = new EventEmitter()
6✔
76
  @Output() urlChange: EventEmitter<string> = new EventEmitter()
6✔
77
  @Output() uploadCancel: EventEmitter<void> = new EventEmitter()
6✔
78
  @Output() delete: EventEmitter<void> = new EventEmitter()
6✔
79
  @Output() altTextChange: EventEmitter<string> = new EventEmitter()
6✔
80

81
  dragFilesOver = false
6✔
82
  showUrlInput = false
6✔
83
  imageFileError = this.uploadError
6✔
84
  showAltTextInput = false
6✔
85

86
  lastUploadType?: 'file' | 'url'
87
  lastUploadContent?: string | File
88
  lastUrl?: string
89

90
  get isUploadInProgress() {
91
    return this.uploadProgress !== undefined
36✔
92
  }
93

94
  constructor(
95
    private http: HttpClient,
6!
96
    private cd: ChangeDetectorRef
6✔
97
  ) {}
98

99
  getPrimaryText() {
100
    if (this.imageFileError) {
6!
101
      return marker('input.image.uploadErrorLabel')
×
102
    }
103
    if (this.uploadProgress) {
6!
104
      return marker('input.image.uploadProgressLabel')
×
105
    }
106
    return marker('input.image.selectFileLabel')
6✔
107
  }
108

109
  getSecondaryText() {
110
    if (this.imageFileError) {
6!
111
      return marker('input.image.uploadErrorRetry')
×
112
    }
113
    if (this.uploadProgress) {
6!
114
      return marker('input.image.uploadProgressCancel')
×
115
    }
116
    return marker('input.image.dropFileLabel')
6✔
117
  }
118

119
  handleDragFilesOver(dragFilesOver: boolean) {
120
    if (!this.showUrlInput) {
×
121
      this.dragFilesOver = dragFilesOver
×
122
      this.cd.markForCheck()
×
123
    }
124
  }
125

126
  handleDropFiles(files: File[]) {
127
    const validFiles = this.filterTypeImage(files)
×
128
    if (validFiles.length > 0) {
×
129
      this.showUrlInput = false
×
130
      this.resizeAndEmit(validFiles[0])
×
131
    } else {
NEW
132
      this.imageFileError = true
×
133
    }
134
  }
135

136
  handleFileInput(event: Event) {
137
    const inputFiles = Array.from((event.target as HTMLInputElement).files)
×
138
    const validFiles = this.filterTypeImage(inputFiles)
×
139
    if (validFiles.length > 0) {
×
140
      this.resizeAndEmit(validFiles[0])
×
141
    }
142
  }
143

144
  displayUrlInput() {
145
    this.uploadCancel.emit()
×
146
    this.showUrlInput = true
×
147
  }
148

149
  onUrlValueChange(url: string) {
NEW
150
    this.lastUrl = url
×
151
  }
152

153
  async downloadUrl(url: string) {
154
    this.imageFileError = false
4✔
155
    const name = url.split('/').pop()
4✔
156
    try {
4✔
157
      const response = await firstValueFrom(
4✔
158
        this.http.head(url, { observe: 'response' })
159
      )
160
      if (
2✔
161
        response.headers.get('content-type')?.startsWith('image/') &&
4✔
162
        parseInt(response.headers.get('content-length')) <
163
          megabytesToBytes(this.maxSizeMB)
164
      ) {
165
        this.http.get(url, { responseType: 'blob' }).subscribe({
2✔
166
          next: (blob) => {
167
            this.cd.markForCheck()
1✔
168
            const file = new File([blob], name)
1✔
169
            this.fileChange.emit(file)
1✔
170
          },
171
          error: () => {
172
            this.imageFileError = true
1✔
173
            this.cd.markForCheck()
1✔
174
            this.urlChange.emit(url)
1✔
175
          },
176
        })
177
      }
178
    } catch {
NEW
179
      this.imageFileError = true
×
180
      this.cd.markForCheck()
×
181
      return
×
182
    }
183
  }
184

185
  handleSecondaryTextClick(event: Event) {
186
    if (this.uploadError) {
×
NEW
187
      this.handleRetryUpload()
×
188
    } else if (this.uploadProgress) {
×
NEW
189
      this.handleCancelUpload()
×
190
      event.preventDefault()
×
NEW
191
    } else if (this.imageFileError && this.lastUrl) {
×
NEW
192
      this.handleRetrySendImgUrl()
×
193
    }
194
  }
195

196
  handleRetrySendImgUrl() {
NEW
197
    this.downloadUrl(this.lastUrl)
×
198
  }
199

200
  handleCancelUpload() {
UNCOV
201
    this.uploadCancel.emit()
×
202
  }
203

204
  handleRetryUpload() {
205
    switch (this.lastUploadType) {
×
206
      case 'file':
207
        this.fileChange.emit(this.lastUploadContent as File)
×
208
        break
×
209
      case 'url':
210
        this.urlChange.emit(this.lastUploadContent as string)
×
211
        break
×
212
    }
213
  }
214

215
  handleDelete() {
216
    this.delete.emit()
×
217
  }
218

219
  toggleAltTextInput() {
220
    this.showAltTextInput = !this.showAltTextInput
×
221
  }
222

223
  handleAltTextChange(altText: string) {
224
    this.altTextChange.emit(altText)
×
225
  }
226

227
  private filterTypeImage(files: File[]) {
228
    return files.filter((file) => {
1✔
229
      return file.type.startsWith('image/')
2✔
230
    })
231
  }
232

233
  private resizeAndEmit(imageToResize: File) {
234
    const maxSizeBytes = megabytesToBytes(this.maxSizeMB)
×
235
    downgradeImage(imageToResize, maxSizeBytes).then((resizedImage) => {
×
236
      const fileToEmit = new File([resizedImage], imageToResize.name)
×
237
      this.fileChange.emit(fileToEmit)
×
238
    })
239
  }
240
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc