Skip to content

Commit 15658f8

Browse files
authored
Create new entity modal and restyle update modal to match (getodk#1480)
1 parent 469e16d commit 15658f8

File tree

10 files changed

+409
-410
lines changed

10 files changed

+409
-410
lines changed

src/components/dataset/entities.vue

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ except according to the terms contained in the LICENSE file.
1919
class="btn btn-primary" @click="upload.show()">
2020
<span class="icon-upload"></span>{{ $t('upload') }}
2121
</button>
22+
<button v-if="project.dataExists && project.permits('entity.create')"
23+
id="dataset-entities-create-button" type="button"
24+
class="btn btn-primary" @click="create.show()">
25+
<span class="icon-plus-circle"></span>{{ $t('new') }}
26+
</button>
2227
<template v-if="deletedEntityCount.dataExists">
2328
<button v-if="canDelete && (deletedEntityCount.value > 0 || deleted)" type="button"
2429
class="btn toggle-deleted-entities" :class="{ 'btn-danger': deleted, 'btn-link': !deleted }"
@@ -42,6 +47,8 @@ except according to the terms contained in the LICENSE file.
4247

4348
<entity-upload v-if="dataset.dataExists" v-bind="upload"
4449
@hide="upload.hide()" @success="afterUpload"/>
50+
<entity-create v-if="dataset.dataExists" v-bind="create"
51+
@hide="create.hide()" @success="afterCreate"/>
4552
<odata-analyze v-bind="analyze" :odata-url="odataUrl" @hide="analyze.hide()"/>
4653
</div>
4754
</template>
@@ -50,6 +57,7 @@ except according to the terms contained in the LICENSE file.
5057
import { defineAsyncComponent, watchEffect, computed } from 'vue';
5158
import { useRouter } from 'vue-router';
5259
60+
import EntityCreate from '../entity/create.vue';
5361
import EntityList from '../entity/list.vue';
5462
import OdataAnalyze from '../odata/analyze.vue';
5563
import OdataDataAccess from '../odata/data-access.vue';
@@ -66,6 +74,7 @@ import { noop } from '../../util/util';
6674
export default {
6775
name: 'DatasetEntities',
6876
components: {
77+
EntityCreate,
6978
OdataAnalyze,
7079
OdataDataAccess,
7180
EntityList,
@@ -114,6 +123,7 @@ export default {
114123
data() {
115124
return {
116125
upload: modalData('EntityUpload'),
126+
create: modalData(),
117127
analyze: modalData()
118128
};
119129
},
@@ -135,6 +145,12 @@ export default {
135145
// reflects the new entities.
136146
this.dataset.entities += count;
137147
},
148+
afterCreate() {
149+
this.create.hide();
150+
this.alert.success(this.$t('alert.create'));
151+
this.$refs.list.reset();
152+
this.dataset.entities += 1;
153+
},
138154
fetchDeletedCount() {
139155
this.deletedEntityCount.request({
140156
method: 'GET',
@@ -197,8 +213,11 @@ export default {
197213
{
198214
"en": {
199215
"upload": "Upload Entities",
216+
// This is shown on a button for creating new Entities
217+
"new": "New",
200218
"alert": {
201-
"upload": "Your Entities have been successfully uploaded."
219+
"upload": "Your Entities have been successfully uploaded.",
220+
"create": "Entity has been successfully created."
202221
},
203222
"purgeDescription": "Entities are deleted after 30 days in the Trash",
204223
"action": {

src/components/entity/create.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<entity-upsert-modal id="entity-create" v-bind="props" create
3+
@hide="emit('hide')" @success="emit('success', $event)"/>
4+
</template>
5+
6+
<script setup>
7+
import EntityUpsertModal from './upsert.vue';
8+
9+
defineOptions({
10+
name: 'EntityCreate'
11+
});
12+
const props = defineProps({
13+
state: Boolean
14+
});
15+
const emit = defineEmits(['hide', 'success']);
16+
</script>

src/components/entity/update.vue

Lines changed: 3 additions & 282 deletions
Original file line numberDiff line numberDiff line change
@@ -10,69 +10,12 @@ including this file, may be copied, modified, propagated, or distributed
1010
except according to the terms contained in the LICENSE file.
1111
-->
1212
<template>
13-
<modal id="entity-update" :state="state" :hideable="!awaitingResponse"
14-
size="large" backdrop @shown="afterShown" @hide="$emit('hide')">
15-
<template #title>{{ $t('title', currentVersion) }}</template>
16-
<template #body>
17-
<form @submit.prevent="submit">
18-
<div class="table-scroll">
19-
<table class="table">
20-
<thead>
21-
<tr>
22-
<th ref="labelCellHeader" class="label-cell">
23-
<span class="sr-only">{{ $t('resource.property') }}</span>
24-
</th>
25-
<th ref="oldValueHeader" class="old-value">
26-
{{ $t('header.currentValue') }}
27-
</th>
28-
<th class="new-value">{{ $t('header.updatedValue') }}</th>
29-
</tr>
30-
</thead>
31-
<tbody>
32-
<entity-update-row ref="labelRow" v-model="label"
33-
:old-value="currentVersion.label"
34-
:label="$t('entity.entityLabel')" required/>
35-
<template v-if="dataset.dataExists">
36-
<entity-update-row v-for="{ name } of dataset.properties"
37-
:key="name" ref="propertyRows" v-model="data[name]"
38-
:old-value="currentVersion.data[name]" :label="name"
39-
:disabled="name === 'geometry' && geometryDisabled"
40-
:disabled-message="$t('geometryDisabled')"/>
41-
</template>
42-
</tbody>
43-
</table>
44-
</div>
45-
<div class="modal-actions">
46-
<button type="button" class="btn btn-link"
47-
:aria-disabled="awaitingResponse" @click="$emit('hide')">
48-
{{ $t('action.neverMind') }}
49-
</button>
50-
<button type="submit" class="btn btn-primary"
51-
:aria-disabled="awaitingResponse">
52-
{{ $t('action.update') }} <spinner :state="awaitingResponse"/>
53-
</button>
54-
</div>
55-
</form>
56-
</template>
57-
</modal>
13+
<entity-upsert-modal id="entity-update" v-bind="props"
14+
@hide="emit('hide')" @success="emit('success', $event)"/>
5815
</template>
5916

6017
<script setup>
61-
import { computed, nextTick, ref, watch } from 'vue';
62-
import { useI18n } from 'vue-i18n';
63-
64-
import EntityUpdateRow from './update/row.vue';
65-
import Modal from '../modal.vue';
66-
import Spinner from '../spinner.vue';
67-
68-
import useColumnGrow from '../../composables/column-grow';
69-
import useRequest from '../../composables/request';
70-
import { apiPaths } from '../../util/request';
71-
import { noop } from '../../util/util';
72-
import { px, styleBox } from '../../util/dom';
73-
import { useRequestData } from '../../request-data';
74-
75-
const { t } = useI18n();
18+
import EntityUpsertModal from './upsert.vue';
7619
7720
defineOptions({
7821
name: 'EntityUpdate'
@@ -83,227 +26,5 @@ const props = defineProps({
8326
entity: Object,
8427
geometryDisabled: Boolean
8528
});
86-
8729
const emit = defineEmits(['hide', 'success']);
88-
89-
const { dataset } = useRequestData();
90-
91-
const label = ref(undefined);
92-
const data = ref(Object.create(null));
93-
watch(() => props.state, (state) => {
94-
if (!state) {
95-
label.value = undefined;
96-
data.value = Object.create(null);
97-
}
98-
});
99-
100-
101-
102-
const { request, awaitingResponse } = useRequest();
103-
const submit = () => {
104-
const { entity } = props;
105-
const url = apiPaths.entity(dataset.projectId, dataset.name, entity.uuid, { baseVersion: entity.currentVersion.version });
106-
107-
request.patch(
108-
url,
109-
{ label: label.value, data: data.value },
110-
{
111-
problemToAlert: ({ code }) => {
112-
if (code === 409.15) return t('problem.409_15');
113-
return null;
114-
}
115-
}
116-
)
117-
.then(response => {
118-
// It is the responsibility of the parent component to patch the entity.
119-
emit('success', response.data);
120-
})
121-
.catch(noop);
122-
};
123-
124-
const labelCellHeader = ref(null);
125-
const { resize: resizeLabelCells } = useColumnGrow(labelCellHeader, 1.5);
126-
const oldValueHeader = ref(null);
127-
const labelRow = ref(null);
128-
// Resizes th.old-value so that the width of the old value and the width of the
129-
// textarea value are the same. Before this resizing, th.old-value and
130-
// th.new-value have the same width. We need to decrease the width of
131-
// th.old-value to account for the padding and borders of the textarea.
132-
const resizeOldValue = () => {
133-
// Remove any width that the function previously set.
134-
oldValueHeader.value.style.width = '';
135-
const { width } = oldValueHeader.value.getBoundingClientRect();
136-
const textarea = styleBox(getComputedStyle(labelRow.value.textarea.el));
137-
const paddingAndBorders = textarea.paddingLeft + textarea.paddingRight +
138-
textarea.borderLeft + textarea.borderRight;
139-
oldValueHeader.value.style.width = px(width - (paddingAndBorders / 2));
140-
};
141-
const propertyRows = ref([]);
142-
const afterShown = () => {
143-
labelRow.value.textarea.focus();
144-
145-
// Resize elements. We wait a tick in case props.entity was changed at the
146-
// same time as props.state. If a change to props.entity results in changes to
147-
// the DOM, we need those changes to the DOM to be made before resizing
148-
// elements based on content in the DOM.
149-
nextTick(() => {
150-
resizeLabelCells();
151-
resizeOldValue();
152-
153-
labelRow.value.textarea.resize();
154-
for (const row of propertyRows.value) row.textarea.resize();
155-
});
156-
};
157-
158-
const noEntity = {
159-
currentVersion: { label: '', data: {} }
160-
};
161-
const currentVersion = computed(() =>
162-
(props.entity ?? noEntity).currentVersion);
16330
</script>
164-
165-
<style lang="scss">
166-
@import '../../assets/scss/variables';
167-
168-
#entity-update {
169-
.modal-dialog { margin-top: 15vh; }
170-
.table-scroll {
171-
max-height: calc(70vh -
172-
#{/* .modal-header */ 46px + /* .modal-actions */ 100px});
173-
overflow-y: auto;
174-
}
175-
176-
table { margin-bottom: 0; }
177-
178-
table { table-layout: fixed; }
179-
thead {
180-
.label-cell { width: 16.66666667%; }
181-
}
182-
183-
tr:nth-child(2) td { border-top-color: #bbb; }
184-
185-
.alert { margin: 15px; }
186-
}
187-
</style>
188-
189-
<i18n lang="json5">
190-
{
191-
"en": {
192-
// This is the title at the top of a pop-up. {label} is the label of an
193-
// Entity.
194-
"title": "Update {label}",
195-
// This is the text of a table column header. "Value" refers to the value of
196-
// an Entity property.
197-
"header": {
198-
"currentValue": "Current Value",
199-
"updatedValue": "Updated Value"
200-
},
201-
"geometryDisabled": "Geometry can’t be updated from the map.",
202-
"problem": {
203-
"409_15": "Data has been modified by another user. Please refresh to see the updated data."
204-
}
205-
}
206-
}
207-
</i18n>
208-
209-
<!-- Autogenerated by destructure.js -->
210-
<i18n>
211-
{
212-
"cs": {
213-
"title": "Aktualizace {label}",
214-
"header": {
215-
"currentValue": "Aktuální hodnota",
216-
"updatedValue": "Aktualizovaná hodnota"
217-
},
218-
"problem": {
219-
"409_15": "Data byla upravena jiným uživatelem. Aktualizujte prosím stránku, abyste viděli aktualizovaná data."
220-
}
221-
},
222-
"de": {
223-
"title": "Aktualisieren {label}",
224-
"header": {
225-
"currentValue": "Aktueller Wert",
226-
"updatedValue": "Aktualisierter Wert"
227-
},
228-
"geometryDisabled": "Die Geometrie kann nicht über die Karte aktualisiert werden.",
229-
"problem": {
230-
"409_15": "Die Daten wurden von einem anderen Benutzer geändert. Bitte aktualisieren Sie die Seite, um die aktualisierten Daten anzuzeigen."
231-
}
232-
},
233-
"es": {
234-
"title": "Actualizar {label}",
235-
"header": {
236-
"currentValue": "Valor actual",
237-
"updatedValue": "Valor actualizado"
238-
},
239-
"geometryDisabled": "La geometría no se puede actualizar desde el mapa.",
240-
"problem": {
241-
"409_15": "Los datos han sido modificados por otro usuario. Actualice para ver los datos actualizados."
242-
}
243-
},
244-
"fr": {
245-
"title": "Mise à jour {label}",
246-
"header": {
247-
"currentValue": "Valeur actuelle",
248-
"updatedValue": "Valeur Mise à jour"
249-
},
250-
"geometryDisabled": "La géométrie ne peut être modifiée depuis la carte.",
251-
"problem": {
252-
"409_15": "Les données ont été modifiées par un autre utilisateur. Merci de rafraîchir pour voir les données mises à jour."
253-
}
254-
},
255-
"it": {
256-
"title": "Aggiorna {label}",
257-
"header": {
258-
"currentValue": "Valore corrente",
259-
"updatedValue": "Valore aggiornato"
260-
},
261-
"geometryDisabled": "La geometria non può essere aggiornata dalla mappa.",
262-
"problem": {
263-
"409_15": "I dati sono stati modificati da un altro utente. Aggiornare per vedere i dati aggiornati."
264-
}
265-
},
266-
"pt": {
267-
"title": "Atualizar {label}",
268-
"header": {
269-
"currentValue": "Valor Atual",
270-
"updatedValue": "Valor Atualizado"
271-
},
272-
"problem": {
273-
"409_15": "Os dados foram modificados por outro usuário. Por favor, atualize a página para ver os dados atualizados."
274-
}
275-
},
276-
"sw": {
277-
"title": "Sasisha {label}",
278-
"header": {
279-
"currentValue": "Thamani ya Sasa",
280-
"updatedValue": "Thamani Iliyosasishwa"
281-
},
282-
"problem": {
283-
"409_15": "Data imerekebishwa na mtumiaji mwingine. Tafadhali onyesha upya ili kuona data iliyosasishwa."
284-
}
285-
},
286-
"zh": {
287-
"title": "更新{label}",
288-
"header": {
289-
"currentValue": "当前数值",
290-
"updatedValue": "更新后的数值"
291-
},
292-
"geometryDisabled": "无法通过地图更新几何数据。",
293-
"problem": {
294-
"409_15": "数据已被其他用户修改,请刷新以查看最新数据。"
295-
}
296-
},
297-
"zh-Hant": {
298-
"title": "更新{label}",
299-
"header": {
300-
"currentValue": "目前數值",
301-
"updatedValue": "更新數值"
302-
},
303-
"geometryDisabled": "無法透過地圖更新地理資料。",
304-
"problem": {
305-
"409_15": "資料已被另一用戶修改。請重新整理查看更新後的資料。"
306-
}
307-
}
308-
}
309-
</i18n>

0 commit comments

Comments
 (0)