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

Freegle / Iznik / 11495

09 May 2026 07:35AM UTC coverage: 69.06% (-3.8%) from 72.847%
11495

Pull #408

circleci

edwh
docs(migration): mark restartproject and repaircafewales as migrated (PR #408)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull Request #408: feat(batch): migrate check_cgas, visualise, tn_sync + dry-run improvements

9127 of 10554 branches covered (86.48%)

Branch coverage included in aggregate %.

507 of 663 new or added lines in 16 files covered. (76.47%)

11902 existing lines in 138 files now uncovered.

101630 of 149824 relevant lines covered (67.83%)

19.56 hits per line

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

68.24
/iznik-nuxt3/components/PostCode.vue
1
<template>
1✔
2
  <div class="d-flex align-items-center">
1✔
3
    <div class="d-flex flex-column">
1✔
4
      <label v-if="label" :for="id">{{ label }}</label>
1✔
5
      <div class="d-flex align-items-center">
1✔
6
        <div
1✔
7
          v-b-tooltip="{
1✔
8
            title: 'Keep typing your full postcode...',
9
            triggers: [],
10
            shown: wip && (!results || results?.length > 1),
11
            placement: 'top',
12
            delay: { show: 3000 },
13
          }"
14
          class="postcode-input-wrapper"
1✔
15
        >
16
          <AutoComplete
1✔
17
            :id="id"
1✔
18
            ref="autocomplete"
1✔
19
            :init-value="wip"
1✔
20
            restrict
1✔
21
            :url="source"
1✔
22
            :custom-params="{ pconly: pconly }"
1✔
23
            anchor="name"
1✔
24
            label=""
1✔
25
            :placeholder="pconly ? 'Type postcode' : 'Type location'"
1✔
26
            :classes="{
1✔
27
              input: 'form-control form-control-' + size + ' text-center pcinp',
28
              list: 'postcodelist',
29
              listentry: 'w-100',
30
              listentrylist: 'listentry',
31
            }"
32
            class="me-1"
1✔
33
            :min="3"
1✔
34
            :debounce="200"
1✔
35
            :process="process"
1✔
36
            :on-select="select"
1✔
37
            :size="10"
1✔
38
            :variant="variant"
1✔
39
            not-found-message="Not a valid postcode."
1✔
40
            @invalid="invalid"
1✔
41
          />
1✔
42
          <v-icon v-if="isValid" icon="check" class="validation-tick" />
1✔
43
        </div>
1✔
44
        <b-popover
1✔
45
          v-if="showLocated"
1✔
46
          content="Your device thinks you're here. If it's wrong, please change it."
1✔
47
          :target="id"
1✔
48
          placement="top"
1✔
49
          variant="primary"
1✔
50
          :show="true"
1✔
51
          :skidding="-50"
1✔
52
        />
1✔
53
        <div v-if="find && !wip">
1✔
54
          <SpinButton
1✔
55
            style="line-height: 1.7em"
1✔
56
            variant="secondary"
1✔
57
            :flex="false"
1✔
58
            button-title="Find my device's location instead of typing a postcode"
1✔
59
            done-icon=""
1✔
60
            :icon-name="
1✔
61
              locationFailed ? 'exclamation-triangle' : 'map-marker-alt'
62
            "
63
            :size="size"
1✔
64
            @handle="findLoc"
1✔
65
          />
1✔
66
        </div>
1✔
67
      </div>
1✔
68
    </div>
1✔
69
  </div>
1✔
70
</template>
71
<script setup>
72
import SpinButton from './SpinButton'
1✔
73
import { uid } from '~/composables/useId'
1✔
74
import { useAuthStore } from '~/stores/auth'
1✔
75
import { useLocationStore } from '~/stores/location'
1✔
76
import {
77
  ref,
78
  computed,
79
  onMounted,
80
  onBeforeUnmount,
81
  useRuntimeConfig,
82
} from '#imports'
1✔
83
import { useComposeStore } from '~/stores/compose'
1✔
84
import AutoComplete from '~/components/AutoComplete'
1✔
85

86
const props = defineProps({
1✔
87
  value: {
88
    type: String,
89
    required: false,
90
    default: null,
91
  },
92
  label: {
93
    type: String,
94
    required: false,
95
    default: null,
96
  },
97
  focus: {
98
    type: Boolean,
99
    required: false,
100
    default: false,
101
  },
102
  find: {
103
    type: Boolean,
104
    required: false,
105
    default: true,
106
  },
107
  size: {
108
    type: String,
109
    required: false,
110
    default: 'lg',
111
  },
112
  pconly: {
113
    type: Boolean,
114
    required: false,
115
    default: true,
116
  },
117
  noStore: {
118
    type: Boolean,
119
    required: false,
120
    default: true,
121
  },
122
  variant: {
123
    type: String,
124
    required: false,
125
    default: null,
126
  },
127
})
128

129
const emit = defineEmits(['selected', 'cleared'])
1✔
130

131
const composeStore = useComposeStore()
1✔
132
const authStore = useAuthStore()
1✔
133
const locationStore = useLocationStore()
1✔
134
const runtimeConfig = useRuntimeConfig()
1✔
135

136
const wip = ref(props.value)
1✔
137
const results = ref([])
1✔
138
const locationFailed = ref(false)
1✔
139
const showLocated = ref(false)
1✔
140
const callbackToCall = ref(null)
1✔
141
const autocomplete = ref(null)
1✔
142
const isValid = ref(false)
1✔
143

144
// Unique id
1✔
145
const id = uid('postcode')
1✔
146

147
const me = authStore.user
1✔
148

149
if (props.pconly && wip.value === null && me?.settings?.mylocation?.name) {
1!
UNCOV
150
  // If we are logged in then we may have a known location to use as the default.
×
UNCOV
151
  wip.value = me?.settings?.mylocation?.name
×
UNCOV
152
}
×
153

154
if (props.pconly && !wip.value && !props.noStore) {
1!
UNCOV
155
  // We might have one we are composing.
×
UNCOV
156
  const pc = composeStore.postcode
×
157

UNCOV
158
  if (pc?.name) {
×
UNCOV
159
    wip.value = pc.name
×
UNCOV
160
  }
×
UNCOV
161
}
×
162

163
const source = computed(() => {
1✔
164
  return runtimeConfig.public.APIv2 + '/location/typeahead'
32✔
165
})
32✔
166

167
function invalid() {
3✔
168
  // Parent might want to know that we don't have a valid postcode any more.
3✔
169
  emit('cleared')
3✔
170
  wip.value = null
3✔
171
  results.value = []
3✔
172
  isValid.value = false
3✔
173
}
3✔
174

175
function keydown(e) {
×
176
  if (e.which === 8) {
×
UNCOV
177
    // Backspace means we no longer have a full postcode.
×
UNCOV
178
    invalid()
×
UNCOV
179
  } else {
×
UNCOV
180
    // Hide the tooltip in case it's showing from a use of the find button.
×
UNCOV
181
    showLocated.value = false
×
UNCOV
182
  }
×
UNCOV
183
}
×
184

185
function process(processResults) {
3✔
186
  const names = []
3✔
187
  const ret = []
3✔
188

189
  if (processResults) {
3✔
190
    for (let i = 0; i < processResults.length && names.length < 5; i++) {
2✔
191
      const loc = processResults[i]
9✔
192

193
      if (!names.includes(loc.name)) {
9✔
194
        names.push(loc.name)
7✔
195
        ret.push(loc)
7✔
196
      }
7✔
197
    }
9✔
198
  }
2✔
199

200
  results.value = ret
3✔
201
  return ret
3✔
202
}
3✔
203

204
async function select(pc) {
7✔
205
  console.log('Select', pc)
7✔
206
  if (pc) {
7✔
207
    if (pc.name && !pc.id) {
5✔
208
      // Find the location this corresponds to.
3✔
209
      const locs = await locationStore.typeahead(pc.name)
3✔
210

211
      if (locs?.length) {
3✔
212
        pc = locs[0]
1✔
213
      }
1✔
214
    }
3✔
215
    emit('selected', pc)
5✔
216
    isValid.value = true
5✔
217
  } else {
7✔
218
    emit('cleared')
2✔
219
    isValid.value = false
2✔
220
  }
2✔
221

222
  locationFailed.value = false
7✔
223
}
7✔
224

225
function findLoc(callback) {
×
226
  callbackToCall.value = callback
×
227

228
  try {
×
UNCOV
229
    if (
×
230
      navigator &&
×
UNCOV
231
      navigator.geolocation &&
×
UNCOV
232
      navigator.geolocation.getCurrentPosition
×
UNCOV
233
    ) {
×
UNCOV
234
      navigator.geolocation.getCurrentPosition(
×
UNCOV
235
        async (position) => {
×
236
          const res = await locationStore.fetchByLatLng(
×
UNCOV
237
            position.coords.latitude,
×
UNCOV
238
            position.coords.longitude
×
239
          )
×
240

UNCOV
241
          if ((res.lat || res.lng) && autocomplete.value) {
×
UNCOV
242
            // Got it - put it in the autocomplete input, and indicate that we've selected it.
×
UNCOV
243
            autocomplete.value.setValue(res.name)
×
UNCOV
244
            await select(res)
×
245

UNCOV
246
            // Show the user we've done this, and make them think.
×
UNCOV
247
            showLocated.value = true
×
UNCOV
248
            setTimeout(() => (showLocated.value = false), 10000)
×
UNCOV
249
          } else {
×
UNCOV
250
            locationFailed.value = true
×
UNCOV
251
          }
×
UNCOV
252
        },
×
UNCOV
253
        (e) => {
×
254
          console.error('Find location failed with', e)
×
UNCOV
255
          locationFailed.value = true
×
UNCOV
256
        }
×
UNCOV
257
      )
×
UNCOV
258
    } else {
×
UNCOV
259
      console.log('Navigation not supported.  ')
×
UNCOV
260
      locationFailed.value = true
×
UNCOV
261
    }
×
UNCOV
262
  } catch (e) {
×
263
    console.error('Find location failed with', e)
×
UNCOV
264
    locationFailed.value = true
×
UNCOV
265
  } finally {
×
266
    callbackToCall.value = null
×
UNCOV
267
    callback()
×
UNCOV
268
  }
×
UNCOV
269
}
×
270

271
onMounted(() => {
1✔
272
  if (autocomplete.value) {
32✔
273
    if (props.focus) {
32!
UNCOV
274
      // Focus on postcode to grab their attention.
×
UNCOV
275
      autocomplete.value.$refs.input.focus()
×
UNCOV
276
    }
×
277

278
    // We need some fettling of the input keystrokes.
32✔
279
    const input = autocomplete.value.$refs.input
32✔
280
    input.addEventListener('keydown', keydown, false)
32✔
281
  } else {
32!
UNCOV
282
    // Not quite sure how this happens, but it does.
×
UNCOV
283
  }
×
284

285
  if (wip.value) {
32✔
286
    select({
2✔
287
      name: wip.value,
2✔
288
    })
2✔
289
  }
2✔
290
})
32✔
291

292
onBeforeUnmount(() => {
1✔
UNCOV
293
  if (callbackToCall.value) {
×
UNCOV
294
    callbackToCall.value()
×
UNCOV
295
  }
×
UNCOV
296
})
×
297
</script>
298
<style scoped lang="scss">
299
@import 'assets/css/_color-vars.scss';
300

301
.postcode-input-wrapper {
302
  position: relative;
303
  display: inline-flex;
304
  align-items: center;
305
}
306

307
:deep(.listentry) {
308
  width: 100%;
309
  right: 0 !important;
310
  text-align: center;
311
  border-color: $color-blue--light;
312
  outline: 0;
313
  box-shadow: 0 1px 0 0.2rem rgba(0, 123, 255, 0.25);
314
}
315

316
:deep(.popover) {
317
  background-color: black;
318
}
319

320
.validation-tick {
321
  position: absolute;
322
  right: 2.5rem;
323
  top: 50%;
324
  transform: translateY(-50%);
325
  color: $color-green-background;
326
  font-size: 1.25rem;
327
  pointer-events: none;
328
  z-index: 10;
329
}
330
</style>
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