Skip to content

Commit

Permalink
Add hover card to show a submission
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-white committed Dec 9, 2024
1 parent 1f040c7 commit 898aa14
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 76 deletions.
11 changes: 4 additions & 7 deletions src/components/entity/basic-details.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ except according to the terms contained in the LICENSE file.
<div v-if="submission != null">
<dt>{{ $t('creatingSubmission') }}</dt>
<dd id="entity-basic-details-creating-submission">
<router-link v-if="submission.currentVersion != null"
:to="submissionPath(projectId, submission.xmlFormId, submission.instanceId)">
{{ submission.currentVersion.instanceName ?? submission.instanceId }}
</router-link>
<submission-link v-if="submission.currentVersion != null"
:project-id="projectId" :xml-form-id="submission.xmlFormId"
:submission="submission"/>
<template v-else>
<span class="icon-trash" v-tooltip.sr-only></span>
<span>{{ submission.instanceId }}</span>
Expand Down Expand Up @@ -55,8 +54,8 @@ import { inject, ref, watchEffect } from 'vue';
import ActorLink from '../actor-link.vue';
import DateTime from '../date-time.vue';
import PageSection from '../page/section.vue';
import SubmissionLink from '../submission/link.vue';

import useRoutes from '../../composables/routes';
import { useRequestData } from '../../request-data';

defineOptions({
Expand All @@ -83,8 +82,6 @@ watchEffect(() => {
submission.value = audit.details.source?.submission;
source.value = audit.details.source;
});

const { submissionPath } = useRoutes();
</script>

<style lang="scss">
Expand Down
18 changes: 5 additions & 13 deletions src/components/entity/feed-entry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ except according to the terms contained in the LICENSE file.
<i18n-t v-if="submission.currentVersion != null"
keypath="title.submission.create.notDeleted">
<template #instanceName>
<router-link :to="sourceSubmissionPath">
{{ submission.currentVersion.instanceName ?? submission.instanceId }}
</router-link>
<submission-link :project-id="projectId"
:xml-form-id="submission.xmlFormId" :submission="submission"/>
</template>
<template #submitter><actor-link :actor="submission.submitter"/></template>
</i18n-t>
Expand Down Expand Up @@ -98,9 +97,8 @@ except according to the terms contained in the LICENSE file.
<i18n-t v-if="submission.currentVersion != null"
keypath="title.entity.update_version.submission.notDeleted">
<template #instanceName>
<router-link :to="sourceSubmissionPath">
{{ submission.currentVersion.instanceName ?? submission.instanceId }}
</router-link>
<submission-link :project-id="projectId"
:xml-form-id="submission.xmlFormId" :submission="submission"/>
</template>
</i18n-t>
<i18n-t v-else keypath="title.entity.update_version.submission.deleted.full">
Expand Down Expand Up @@ -146,9 +144,9 @@ import ActorLink from '../actor-link.vue';
import DatasetLink from '../dataset/link.vue';
import EntityDiff from './diff.vue';
import FeedEntry from '../feed-entry.vue';
import SubmissionLink from '../submission/link.vue';
import SubmissionReviewState from '../submission/review-state.vue';

import useRoutes from '../../composables/routes';
import { useRequestData } from '../../request-data';

defineOptions({
Expand Down Expand Up @@ -180,12 +178,6 @@ const wrapTitle = computed(() => {
});

// submission.create, entity.update.version
const { submissionPath } = useRoutes();
const sourceSubmissionPath = computed(() => submissionPath(
projectId,
props.submission.xmlFormId,
props.submission.instanceId
));
const { t } = useI18n();
const deletedSubmission = (key) => t(key, { id: props.submission.instanceId });

Expand Down
63 changes: 63 additions & 0 deletions src/components/hover-card/submission.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!--
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>
<hover-card icon="tag" :truncate-dt="false">
<template #title>
<template v-if="submission.instanceName != null">
{{ submission.instanceName }}
</template>
<template v-else>
<span v-for="(part, index) in idParts" :key="index">{{ part }}</span>
</template>
</template>
<template #subtitle>{{ $t('resource.submission') }}</template>
<template #body>
<dl class="dl-horizontal">
<dt>{{ $t('resource.form') }}</dt>
<dd>{{ form.nameOrId }}</dd>

<dt>{{ $t('header.submitterName') }}</dt>
<dd>{{ submission.__system.submitterName }}</dd>

<dt>{{ $t('header.submissionDate') }}</dt>
<dd><date-time :iso="submission.__system.submissionDate"/></dd>

<dt>{{ $t('common.reviewState') }}</dt>
<dd>
<submission-review-state :value="submission.__system.reviewState"/>
</dd>
</dl>
</template>
</hover-card>
</template>

<script setup>
import { computed } from 'vue';

import DateTime from '../date-time.vue';
import HoverCard from '../hover-card.vue';
import SubmissionReviewState from '../submission/review-state.vue';

import { useRequestData } from '../../request-data';

defineOptions({
name: 'HoverCardSubmission'
});

const { form, submission } = useRequestData();

const idParts = computed(() => {
const id = submission.__id;
const match = id.match(/^((?:uuid:)?[0-9a-f]{8})[0-9a-f-]+([0-9a-f]{8})$/);
return match != null ? [match[1], '…', match[2]] : [id];
});
</script>
12 changes: 12 additions & 0 deletions src/components/hover-cards.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { inject, nextTick, ref, shallowRef, watch } from 'vue';
import HoverCardDataset from './hover-card/dataset.vue';
import HoverCardEntity from './hover-card/entity.vue';
import HoverCardForm from './hover-card/form.vue';
import HoverCardSubmission from './hover-card/submission.vue';
import Popover from './popover.vue';

import useHoverCardResources from '../request-data/hover-card';
Expand All @@ -47,6 +48,17 @@ const types = {
form: { url: apiPaths.form(projectId, xmlFormId), extended: true }
})
},
submission: {
component: HoverCardSubmission,
requests: ({ projectId, xmlFormId, instanceId }) => ({
form: { url: apiPaths.form(projectId, xmlFormId) },
submission: {
url: apiPaths.odataSubmission(projectId, xmlFormId, instanceId, {
$select: '__id,__system,meta'
})
}
})
},
dataset: {
component: HoverCardDataset,
requests: ({ projectId, name }) => ({
Expand Down
58 changes: 58 additions & 0 deletions src/components/submission/link.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!--
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.
-->

<!-- Specifying :key so that if `to` changes, the element will be replaced. If a
hover card is shown next to the element, it will be hidden. -->
<template>
<router-link ref="link" :key="to" :to="to">
{{ submission.currentVersion.instanceName ?? submission.instanceId }}
</router-link>
</template>

<script setup>
import { computed, ref } from 'vue';

import useHoverCard from '../../composables/hover-card';
import useRoutes from '../../composables/routes';

defineOptions({
name: 'SubmissionLink'
});
const props = defineProps({
projectId: {
type: [Number, String],
required: true
},
xmlFormId: {
type: String,
required: true
},
// A submission in the format of a REST API response (not OData). It is not
// expected to be a transformed submission resource. For example,
// props.submission.instanceNameOrId is not expected to be defined.
submission: {
type: Object,
required: true
}
});

const { submissionPath } = useRoutes();
const to = computed(() =>
submissionPath(props.projectId, props.xmlFormId, props.submission.instanceId));

const link = ref(null);
useHoverCard(computed(() => link.value?.$el), 'submission', () => ({
projectId: props.projectId,
xmlFormId: props.xmlFormId,
instanceId: props.submission.instanceId
}));
</script>
1 change: 1 addition & 0 deletions src/locales/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@
"webUser": "Web User",
"webUsers": "Web Users",
"session": "Session",
"submission": "Submission",
"submissions": "Submissions",
"projectRoles": "Project Roles",
"formPreview": "Form Preview"
Expand Down
3 changes: 3 additions & 0 deletions src/request-data/hover-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ except according to the terms contained in the LICENSE file.
// e.g., `form`. For simplicity, we want the hover card resources to be
// independent of resources used in other components.

import useSubmission from './submission';
import { transformForm } from './util';
import { useRequestData } from './index';

export default () => {
const { createResource } = useRequestData();
const { submission } = useSubmission();
return {
form: createResource('form', () => ({
transformResponse: ({ data }) => transformForm(data)
})),
submission,
dataset: createResource('dataset'),
entity: createResource('entity')
};
Expand Down
12 changes: 7 additions & 5 deletions src/request-data/submission.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export default () => {
result.__system = reactive(result.__system);
return result;
},
instanceNameOrId: computeIfExists(() => {
instanceName: computeIfExists(() => {
const { meta } = submission;
return meta != null && typeof meta.instanceName === 'string'
? meta.instanceName
: submission.__id;
})
if (meta == null || typeof meta !== 'object') return null;
const { instanceName } = meta;
return typeof instanceName === 'string' ? instanceName : null;
}),
instanceNameOrId: computeIfExists(() =>
submission.instanceName ?? submission.__id)
}));
const submissionVersion = createResource('submissionVersion');
const audits = createResource('audits');
Expand Down
38 changes: 7 additions & 31 deletions test/components/entity/basic-details.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { RouterLinkStub } from '@vue/test-utils';

import ActorLink from '../../../src/components/actor-link.vue';
import DateTime from '../../../src/components/date-time.vue';
import EntityBasicDetails from '../../../src/components/entity/basic-details.vue';
import SubmissionLink from '../../../src/components/submission/link.vue';

import useEntity from '../../../src/request-data/entity';

Expand Down Expand Up @@ -41,11 +40,10 @@ describe('EntityBasicDetails', () => {

describe('creating submission', () => {
const createEntityFromSubmission = ({
deleted: submissionDeleted = false,
...submissionOptions
deleted: submissionDeleted = false
} = {}) => {
const submission = testData.extendedSubmissions
.createPast(1, { instanceId: 's', ...submissionOptions })
.createPast(1, { instanceId: 's' })
.last();
testData.extendedEntities.createPast(1, { uuid: 'e' });
const details = {
Expand All @@ -66,42 +64,20 @@ describe('EntityBasicDetails', () => {
};

it('shows creating submission if there is a submission in audit log', () => {
createEntityFromSubmission();
const component = mountComponent();
const dd = component.find('#entity-basic-details-creating-submission');
dd.exists().should.be.true;
});

it('shows the instance name if the submission has one', () => {
createEntityFromSubmission({
meta: { instanceName: 'My Submission' }
});
const component = mountComponent();
const dd = component.get('#entity-basic-details-creating-submission');
dd.text().should.equal('My Submission');
});

it('falls back to showing the instance ID', () => {
createEntityFromSubmission();
const component = mountComponent();
const dd = component.get('#entity-basic-details-creating-submission');
dd.text().should.equal('s');
});

it('links to the submission', () => {
createEntityFromSubmission();
const component = mountComponent();
const dd = component.get('#entity-basic-details-creating-submission');
const { to } = dd.getComponent(RouterLinkStub).props();
to.should.equal('/projects/1/forms/f/submissions/s');
const link = dd.getComponent(SubmissionLink);
link.props().should.include({ projectId: '1', xmlFormId: 'f' });
link.props().submission.instanceId.should.equal('s');
});

describe('submission was deleted', () => {
it('does not link to the submission', () => {
createEntityFromSubmission({ deleted: true });
const component = mountComponent();
const dd = component.get('#entity-basic-details-creating-submission');
dd.findComponent(RouterLinkStub).exists().should.be.false;
dd.findComponent(SubmissionLink).exists().should.be.false;
});

it('shows a trash icon with a tooltip', async () => {
Expand Down
Loading

0 comments on commit 898aa14

Please sign in to comment.