Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add first version of entities upload modal #941

Merged
merged 2 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions src/components/dataset/entities.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,53 @@ https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->

<template>
<div>
<page-section>
<template #heading>
<span>{{ $t('resource.entities') }}</span>
<button v-if="project.dataExists && project.permits('entity.create')"
id="dataset-entities-upload-button" type="button"
class="btn btn-primary" @click="showModal('upload')">
<span class="icon-upload"></span>{{ $t('action.upload') }}
</button>
<odata-data-access @analyze="showModal('analyze')"/>
</template>
<template #body>
<entity-list :project-id="projectId" :dataset-name="datasetName"/>
</template>
</page-section>

<entity-upload v-if="dataset.dataExists" v-bind="upload"
@hide="hideModal('upload')" @success="afterUpload"/>
<odata-analyze v-bind="analyze" :odata-url="odataUrl" @hide="hideModal('analyze')"/>
</div>
</template>

<script>
import { defineAsyncComponent } from 'vue';

import EntityList from '../entity/list.vue';
import OdataAnalyze from '../odata/analyze.vue';
import OdataDataAccess from '../odata/data-access.vue';
import EntityList from '../entity/list.vue';
import PageSection from '../page/section.vue';

import modal from '../../mixins/modal';
import { apiPaths } from '../../util/request';
import { loadAsync } from '../../util/load-async';
import { useRequestData } from '../../request-data';

export default {
name: 'DatasetEntities',
components: {
OdataAnalyze,
OdataDataAccess,
EntityList,
EntityUpload: defineAsyncComponent(loadAsync('EntityUpload')),
PageSection
},
mixins: [modal()],
mixins: [modal({ upload: 'EntityUpload' })],
inject: ['alert'],
props: {
projectId: {
type: String,
Expand All @@ -53,8 +66,15 @@ export default {
required: true
}
},
setup() {
const { project, dataset } = useRequestData();
return { project, dataset };
},
data() {
return {
upload: {
state: false
},
analyze: {
state: false
}
Expand All @@ -65,6 +85,12 @@ export default {
const path = apiPaths.odataEntitiesSvc(this.projectId, this.datasetName);
return `${window.location.origin}${path}`;
}
},
methods: {
afterUpload() {
this.hideModal('upload');
this.alert.success('Entities were imported successfully! [TODO: i18n]');
}
}
};
</script>
Expand Down
84 changes: 84 additions & 0 deletions src/components/entity/upload.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!--
Copyright 2024 ODK Central Developers
See the NOTICE file at the top-level directory of this distribution and at
https://github.com/getodk/central-frontend/blob/master/NOTICE.

This file is part of ODK Central. It is subject to the license terms in
the LICENSE file found in the top-level directory of this distribution and at
https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->
<template>
<modal id="entity-upload" :state="state" :hideable="!awaitingResponse"
backdrop @hide="$emit('hide')">
<template #title>{{ $t('title') }}</template>
<template #body>
<div>
<span>{{ $t('headersNote') }}</span>
<sentence-separator/>
<entity-upload-data-template/>
</div>

<div class="modal-actions">
<button type="button" class="btn btn-primary"
:aria-disabled="awaitingResponse" @click="upload">
{{ $t('action.append') }} <spinner :state="awaitingResponse"/>
</button>
<button type="button" class="btn btn-link"
:aria-disabled="awaitingResponse" @click="$emit('hide')">
{{ $t('action.cancel') }}
</button>
</div>
</template>
</modal>
</template>

<script setup>
import EntityUploadDataTemplate from './upload/data-template.vue';
import Modal from '../modal.vue';
import SentenceSeparator from '../sentence-separator.vue';
import Spinner from '../spinner.vue';

import useRequest from '../../composables/request';
import { apiPaths } from '../../util/request';
import { noop } from '../../util/util';
import { useRequestData } from '../../request-data';

defineOptions({
name: 'EntityUpload'
});
defineProps({
state: Boolean
});
const emit = defineEmits(['hide', 'success']);

const { dataset } = useRequestData();

const { request, awaitingResponse } = useRequest();
const upload = () => {
request({
method: 'POST',
url: apiPaths.entities(dataset.projectId, dataset.name),
data: {
source: { name: 'TODO' },
entities: []
}
})
.then(() => { emit('success'); })
.catch(noop);
};
</script>

<i18n lang="json5">
{
"en": {
// This is the title at the top of a pop-up.
"title": "Import Data from File",
"headersNote": "The first row in your data file must exactly match the table header you see above.",
"action": {
"append": "Append data"
}
}
}
</i18n>
1 change: 0 additions & 1 deletion src/components/form/new.vue
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ export default {
"chooseOne": "choose one"
},
"action": {
"upload": "Upload",
"uploadAnyway": "Upload anyway"
},
"alert": {
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@
// This text is shown next to a dropdown menu or other way to change the sorting of Projects or Forms
"sort": "Sort",
"update": "Update",
// @transifexKey component.FormNew.action.upload
"upload": "Upload",
"yesProceed": "Yes, proceed",
// This text is shown on a button that the user clicks to show a preview of something
"showPreview": "Preview",
Expand Down
4 changes: 4 additions & 0 deletions src/util/load-async.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ const loaders = new Map()
/* webpackChunkName: "component-entity-show" */
'../components/entity/show.vue'
)))
.set('EntityUpload', loader(() => import(
/* webpackChunkName: "component-entity-upload" */
'../components/entity/upload.vue'
)))
.set('FieldKeyList', loader(() => import(
/* webpackChunkName: "component-field-key-list" */
'../components/field-key/list.vue'
Expand Down
83 changes: 83 additions & 0 deletions test/components/entity/upload.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import EntityUpload from '../../../src/components/entity/upload.vue';

import testData from '../../data';
import { mockHttp, load } from '../../util/http';
import { mockLogin } from '../../util/session';

const mountOptions = () => {
const dataset = testData.extendedDatasets.last();
return {
props: { state: true },
container: {
requestData: { dataset }
}
};
};

describe('EntityUpload', () => {
it('toggles the modal', () => {
mockLogin();
testData.extendedDatasets.createPast(1);
return load('/projects/1/entity-lists/trees/entities', { root: false })
.testModalToggles({
modal: EntityUpload,
show: '#dataset-entities-upload-button',
hide: '.btn-link'
});
});

it('does not render the upload button for a project viewer', async () => {
mockLogin({ role: 'none' });
testData.extendedProjects.createPast(1, { role: 'viewer', datasets: 1 });
testData.extendedDatasets.createPast(1);
const component = await load('/projects/1/entity-lists/trees/entities', {
root: false
});
const button = component.find('#dataset-entities-upload-button');
button.exists().should.be.false();
});

it('sends the correct upload request', () => {
testData.extendedDatasets.createPast(1, { name: 'á' });
return mockHttp()
.mount(EntityUpload, mountOptions())
.complete()
.request(modal => modal.get('.btn-primary').trigger('click'))
.respondWithProblem()
.testRequests([{
method: 'POST',
url: '/v1/projects/1/datasets/%C3%A1/entities',
data: {
source: { name: 'TODO' },
entities: []
}
}]);
});

it('implements some standard button things', () => {
testData.extendedDatasets.createPast(1);
return mockHttp()
.mount(EntityUpload, mountOptions())
.testStandardButton({
button: '.btn-primary',
disabled: ['.btn-link'],
modal: true
});
});

describe('after a successful upload', () => {
it('hides the modal', () => {
testData.extendedDatasets.createPast(1);
return load('/projects/1/entity-lists/trees/entities', { root: false })
.complete()
.request(async (component) => {
await component.get('#dataset-entities-upload-button').trigger('click');
return component.get('#entity-upload .btn-primary').trigger('click');
})
.respondWithSuccess()
.afterResponse(component => {
component.getComponent(EntityUpload).props().state.should.be.false();
});
});
});
});
23 changes: 19 additions & 4 deletions transifex/strings_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1909,6 +1909,21 @@
}
}
},
"EntityUpload": {
"title": {
"string": "Import Data from File",
"developer_comment": "This is the title at the top of a pop-up."
},
"headersNote": {
"string": "The first row in your data file must exactly match the table header you see above."
},
"action": {
"append": {
"string": "Append data",
"developer_comment": "This is the text for an action, for example, the text of a button."
}
}
},
"EntityUploadDataTemplate": {
"text": {
"full": {
Expand Down Expand Up @@ -2765,13 +2780,13 @@
}
},
"action": {
"upload": {
"string": "Upload",
"developer_comment": "This is the text for an action, for example, the text of a button."
},
"uploadAnyway": {
"string": "Upload anyway",
"developer_comment": "This is the text for an action, for example, the text of a button."
},
"upload": {
"string": "Upload",
"developer_comment": "This is the text for an action, for example, the text of a button."
}
},
"alert": {
Expand Down