Skip to content

Commit 2c9c13f

Browse files
fix: import
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
1 parent 6d6d24c commit 2c9c13f

17 files changed

Lines changed: 1416 additions & 663 deletions

File tree

src/components/AppNavigation/ContactsSettings.vue

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@
2222
@update:model-value="toggleSocialSync" />
2323
</NcFormBox>
2424

25-
<SettingsImportContacts
26-
:addressbooks="addressbooks"
27-
@clicked="onClickImport"
28-
@file-loaded="onLoad" />
25+
<SettingsImportContacts />
2926
</AppSettingsSection>
3027

3128
<AppSettingsSection id="address-books" :name="t('contacts', 'Address books')">
@@ -105,10 +102,6 @@ export default {
105102
},
106103
107104
methods: {
108-
onClickImport(event) {
109-
this.$emit('clicked', event)
110-
},
111-
112105
async toggleSocialSync(value) {
113106
this.enableSocialSyncLoading = true
114107
@@ -126,10 +119,6 @@ export default {
126119
}
127120
},
128121
129-
onLoad() {
130-
this.$emit('file-loaded', false)
131-
},
132-
133122
async onOpen() {
134123
this.showSettings = true
135124
},
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<Modal
8+
class="import-modal"
9+
size="normal"
10+
:name="t('contacts', 'Import destination selection')"
11+
@close="cancelImport">
12+
<template v-if="isSelecting">
13+
<transition-group class="import-modal__file-list" tag="ul">
14+
<ImportScreenRow v-for="entry in entries" :key="`import-file-${entry.file.id}`" :entry="entry" />
15+
</transition-group>
16+
17+
<div class="import-modal__actions">
18+
<NcButton @click="cancelImport">
19+
{{ t('contacts', 'Cancel') }}
20+
</NcButton>
21+
<NcButton variant="primary" @click="importContacts">
22+
<template #icon>
23+
<Upload :size="20" />
24+
</template>
25+
{{ n('contacts', 'Import contacts', 'Import contacts', entries.length) }}
26+
</NcButton>
27+
</div>
28+
</template>
29+
30+
<template v-else>
31+
<h4 class="import-modal__title">
32+
{{ activeFileLabel }}
33+
</h4>
34+
35+
<NcProgressBar class="import-modal__progress-bar" :value="progressValue" size="medium" />
36+
37+
<div class="import-modal__counters">
38+
<NcNoteCard type="info">
39+
<strong>{{ totals.discovered }}</strong> {{ t('contacts', 'Discovered') }}
40+
</NcNoteCard>
41+
<NcNoteCard type="info">
42+
<strong>{{ totals.processed }}</strong> {{ t('contacts', 'Processed') }}
43+
</NcNoteCard>
44+
<NcNoteCard type="success">
45+
<strong>{{ totals.created }}</strong> {{ t('contacts', 'Created') }}
46+
</NcNoteCard>
47+
<NcNoteCard type="info">
48+
<strong>{{ totals.updated }}</strong> {{ t('contacts', 'Updated') }}
49+
</NcNoteCard>
50+
<NcNoteCard type="warning">
51+
<strong>{{ totals.exists }}</strong> {{ t('contacts', 'Skipped') }}
52+
</NcNoteCard>
53+
<NcNoteCard type="error">
54+
<strong>{{ totals.error }}</strong> {{ t('contacts', 'Errors') }}
55+
</NcNoteCard>
56+
</div>
57+
</template>
58+
</Modal>
59+
</template>
60+
61+
<script>
62+
import { NcModal as Modal, NcButton, NcNoteCard, NcProgressBar } from '@nextcloud/vue'
63+
import Upload from 'vue-material-design-icons/TrayArrowUp.vue'
64+
import ImportScreenRow from './ImportScreenRow.vue'
65+
66+
export default {
67+
name: 'ImportScreen',
68+
components: {
69+
NcButton,
70+
NcNoteCard,
71+
NcProgressBar,
72+
ImportScreenRow,
73+
Modal,
74+
Upload,
75+
},
76+
77+
props: {
78+
entries: {
79+
type: Array,
80+
required: true,
81+
},
82+
83+
stage: {
84+
type: String,
85+
required: true,
86+
},
87+
88+
totals: {
89+
type: Object,
90+
required: true,
91+
},
92+
93+
activeSession: {
94+
type: Object,
95+
default: null,
96+
},
97+
},
98+
99+
emits: ['cancelImport', 'importContacts'],
100+
101+
computed: {
102+
isSelecting() {
103+
return this.stage === 'selecting'
104+
},
105+
106+
progressValue() {
107+
return Math.round(this.totals.processed / Math.max(this.totals.discovered, 1) * 100)
108+
},
109+
110+
activeFileLabel() {
111+
if (!this.activeSession) {
112+
return t('contacts', 'Preparing import…')
113+
}
114+
115+
return t('contacts', 'Importing {fileName} into {addressbook}', {
116+
fileName: this.activeSession.fileName,
117+
addressbook: this.activeSession.targetDisplayName || t('contacts', 'selected address book'),
118+
})
119+
},
120+
},
121+
122+
methods: {
123+
importContacts() {
124+
this.$emit('importContacts')
125+
},
126+
127+
cancelImport() {
128+
this.$emit('cancelImport')
129+
},
130+
},
131+
}
132+
</script>
133+
134+
<style lang="scss" scoped>
135+
.import-modal {
136+
&__file-list {
137+
display: flex;
138+
flex-direction: column;
139+
gap: 12px;
140+
padding: 12px;
141+
margin: 0;
142+
}
143+
144+
&__actions {
145+
display: flex;
146+
justify-content: flex-end;
147+
gap: 8px;
148+
padding: 12px;
149+
}
150+
151+
&__title {
152+
padding: 12px 12px 0;
153+
text-align: center;
154+
}
155+
156+
&__progress-bar {
157+
margin: 12px;
158+
width: calc(100% - 24px);
159+
}
160+
161+
&__counters {
162+
display: grid;
163+
grid-template-columns: repeat(2, 1fr);
164+
gap: 8px;
165+
padding: 12px;
166+
}
167+
}
168+
</style>
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<li class="import-modal-file-item">
8+
<NcFormGroup :label="t('contacts', 'Address book to import into')">
9+
<NcFormBox v-slot="{ itemClass }">
10+
<NcFormBoxCopyButton :class="itemClass" :label="t('contacts', 'File')" :value="file.name" />
11+
</NcFormBox>
12+
<NcFormBox v-slot="{ itemClass }">
13+
<div class="import-modal-file-item__field" :class="itemClass">
14+
<label :for="addressbookInputId" class="import-modal-file-item__field-label">
15+
{{ t('contacts', 'Address book') }}
16+
</label>
17+
<NcSelect
18+
v-model="selectedAddressbook"
19+
:input-id="addressbookInputId"
20+
:options="addressbookOptions"
21+
:allow-empty="false"
22+
:clearable="false"
23+
label="displayName" />
24+
</div>
25+
</NcFormBox>
26+
</NcFormGroup>
27+
<NcFormGroup :label="t('contacts', 'Import options')">
28+
<NcFormBox v-slot="{ itemClass }">
29+
<div class="import-modal-file-item__field" :class="itemClass">
30+
<label :for="formatInputId" class="import-modal-file-item__field-label">
31+
{{ t('contacts', 'File format') }}
32+
</label>
33+
<NcSelect
34+
v-model="selectedFormat"
35+
:input-id="formatInputId"
36+
:options="formatOptions"
37+
:allow-empty="false"
38+
:clearable="false"
39+
label="label" />
40+
</div>
41+
</NcFormBox>
42+
<NcFormBox v-slot="{ itemClass }">
43+
<NcFormBoxSwitch
44+
:class="itemClass"
45+
:model-value="supersede"
46+
:label="t('contacts', 'Overwrite existing contacts')"
47+
:description="t('contacts', 'Replace contacts in the address book that match the imported ones instead of skipping them.')"
48+
@update:model-value="setSupersede" />
49+
</NcFormBox>
50+
</NcFormGroup>
51+
</li>
52+
</template>
53+
54+
<script>
55+
import { NcFormBox, NcFormBoxCopyButton, NcFormBoxSwitch, NcFormGroup, NcSelect } from '@nextcloud/vue'
56+
import { mapStores } from 'pinia'
57+
import useImportStore from '../../../store/import.ts'
58+
59+
const NEW_ADDRESSBOOK_ID = 'new'
60+
61+
export default {
62+
name: 'ImportScreenRow',
63+
components: {
64+
NcFormBox,
65+
NcFormBoxCopyButton,
66+
NcFormBoxSwitch,
67+
NcFormGroup,
68+
NcSelect,
69+
},
70+
71+
props: {
72+
entry: {
73+
type: Object,
74+
required: true,
75+
},
76+
},
77+
78+
computed: {
79+
...mapStores(useImportStore),
80+
81+
file() {
82+
return this.entry.file
83+
},
84+
85+
addressbookInputId() {
86+
return `import-addressbook-${this.file.id}`
87+
},
88+
89+
formatInputId() {
90+
return `import-format-${this.file.id}`
91+
},
92+
93+
formatOptions() {
94+
return [{
95+
id: 'ical',
96+
label: t('contacts', 'vCard (.vcf)'),
97+
}, {
98+
id: 'jcal',
99+
label: t('contacts', 'jCard (.json)'),
100+
}, {
101+
id: 'xcal',
102+
label: t('contacts', 'xCard (.xml)'),
103+
}]
104+
},
105+
106+
selectedFormat: {
107+
get() {
108+
const format = this.entry.options.format
109+
return this.formatOptions.find((option) => option.id === format) ?? this.formatOptions[0]
110+
},
111+
112+
set(option) {
113+
if (!option) {
114+
return
115+
}
116+
this.importStore.setOptionsForFile({
117+
fileId: this.file.id,
118+
options: { format: option.id },
119+
})
120+
},
121+
},
122+
123+
supersede() {
124+
return this.entry.options.supersede
125+
},
126+
127+
newAddressbook() {
128+
return {
129+
id: NEW_ADDRESSBOOK_ID,
130+
displayName: t('contacts', 'New address book'),
131+
}
132+
},
133+
134+
writableAddressbooks() {
135+
return this.$store.getters.getAddressbooks
136+
.filter((addressbook) => !addressbook.readOnly && addressbook.enabled && addressbook.canCreateCard)
137+
.map((addressbook) => ({ id: addressbook.id, displayName: addressbook.displayName }))
138+
},
139+
140+
addressbookOptions() {
141+
return [...this.writableAddressbooks, this.newAddressbook]
142+
},
143+
144+
selectedAddressbook: {
145+
get() {
146+
const addressbookId = this.entry.addressbookId
147+
return this.addressbookOptions.find((option) => option.id === addressbookId) ?? this.addressbookOptions[0]
148+
},
149+
150+
set(option) {
151+
if (!option) {
152+
return
153+
}
154+
this.importStore.setAddressbookForFile({
155+
fileId: this.file.id,
156+
addressbookId: option.id,
157+
})
158+
},
159+
},
160+
},
161+
162+
created() {
163+
const preselected = this.addressbookOptions[0]
164+
this.importStore.setAddressbookForFile({
165+
fileId: this.file.id,
166+
addressbookId: preselected.id,
167+
})
168+
},
169+
170+
methods: {
171+
setSupersede(supersede) {
172+
this.importStore.setOptionsForFile({
173+
fileId: this.file.id,
174+
options: { supersede },
175+
})
176+
},
177+
},
178+
}
179+
</script>
180+
181+
<style lang="scss" scoped>
182+
.import-modal-file-item__field {
183+
display: flex;
184+
flex-direction: column;
185+
gap: 4px;
186+
padding: 8px 12px;
187+
188+
&-label {
189+
font-weight: bold;
190+
}
191+
}
192+
</style>

0 commit comments

Comments
 (0)