Skip to content

Commit

Permalink
Add hover card to show a form
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-white committed Dec 9, 2024
1 parent e338e52 commit 1f040c7
Show file tree
Hide file tree
Showing 17 changed files with 329 additions and 182 deletions.
10 changes: 6 additions & 4 deletions src/components/audit/row.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { pick } from 'ramda';

import ActorLink from '../actor-link.vue';
import DatasetLink from '../dataset/link.vue';
import FormLink from '../form/link.vue';
import DateTime from '../date-time.vue';
import Selectable from '../selectable.vue';

Expand Down Expand Up @@ -93,7 +94,8 @@ const acteeSpeciesByCategory = {
},
form: {
title: (actee) => (actee.name != null ? actee.name : actee.xmlFormId),
path: (actee, { primaryFormPath }) => primaryFormPath(actee)
component: FormLink,
props: (actee) => ({ form: actee })
},
dataset: {
title: (actee) => actee.name,
Expand All @@ -114,7 +116,7 @@ acteeSpeciesByCategory.upgrade = acteeSpeciesByCategory.form;

export default {
name: 'AuditRow',
components: { ActorLink, DatasetLink, DateTime, Selectable },
components: { ActorLink, DatasetLink, DateTime, FormLink, Selectable },
props: {
audit: {
type: Object,
Expand All @@ -123,8 +125,8 @@ export default {
},
setup() {
const { actionMessage } = useAudit();
const { projectPath, primaryFormPath, userPath } = useRoutes();
return { actionMessage, projectPath, primaryFormPath, userPath };
const { projectPath, userPath } = useRoutes();
return { actionMessage, projectPath, userPath };
},
computed: {
// When an audit log action has multiple parts/segments, we treat it as
Expand Down
50 changes: 50 additions & 0 deletions src/components/form/link.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!--
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 the link path changes, the element will be
replaced. If a hover card is shown next to the element, it will be hidden. -->
<template>
<link-if-can v-if="to == null" ref="link" :key="toDefault" :to="toDefault">
{{ form.name ?? form.xmlFormId }}
</link-if-can>
<router-link v-else ref="link" :key="to" :to="to">
{{ form.name ?? form.xmlFormId }}
</router-link>
</template>

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

import LinkIfCan from '../link-if-can.vue';

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

defineOptions({
name: 'FormLink'
});
const props = defineProps({
// This may be a transformed form resource, but it is not required to be. For
// example, props.form.nameOrId may or may not be defined.
form: {
type: Object,
required: true
},
to: String
});

const { primaryFormPath } = useRoutes();
const toDefault = computed(() => primaryFormPath(props.form));

const link = ref(null);
useHoverCard(computed(() => link.value?.$el), 'form', () => props.form);
</script>
17 changes: 11 additions & 6 deletions src/components/form/row.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ except according to the terms contained in the LICENSE file.
<template>
<tr class="form-row">
<td class="name">
<link-if-can :to="primaryFormPath(form)">
{{ form.nameOrId }}
</link-if-can>
<form-link :form="form"/>
<span v-if="showIdForDuplicateName" class="duplicate-form-id">({{ form.xmlFormId }})</span>
</td>

Expand Down Expand Up @@ -89,6 +87,7 @@ import { DateTime } from 'luxon';
import DateTimeComponent from '../date-time.vue';
import EnketoFill from '../enketo/fill.vue';
import EnketoPreview from '../enketo/preview.vue';
import FormLink from './link.vue';
import LinkIfCan from '../link-if-can.vue';

import useReviewState from '../../composables/review-state';
Expand All @@ -98,7 +97,13 @@ import { useRequestData } from '../../request-data';

export default {
name: 'FormRow',
components: { DateTime: DateTimeComponent, EnketoFill, EnketoPreview, LinkIfCan },
components: {
DateTime: DateTimeComponent,
EnketoFill,
EnketoPreview,
FormLink,
LinkIfCan
},
props: {
form: {
type: Object,
Expand All @@ -111,11 +116,11 @@ export default {
},
setup() {
const { project, duplicateFormNames } = useRequestData();
const { formPath, primaryFormPath } = useRoutes();
const { formPath } = useRoutes();
const { reviewStateIcon } = useReviewState();
return {
project, duplicateFormNames,
formPath, primaryFormPath,
formPath,
reviewStateIcon
};
},
Expand Down
43 changes: 43 additions & 0 deletions src/components/hover-card/form.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
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="file" :truncate-dt="false">
<template #title>{{ form.nameOrId }}</template>
<template #subtitle>{{ $t('resource.form') }}</template>
<template #body>
<dl class="dl-horizontal">
<dt>{{ $t('common.version') }}</dt>
<dd><form-version-string :version="form.version"/></dd>

<dt>{{ $t('resource.submissions') }}</dt>
<dd>{{ $n(form.submissions, 'default') }}</dd>

<dt>{{ $t('header.lastSubmission') }}</dt>
<dd><date-time :iso="form.lastSubmission"/></dd>
</dl>
</template>
</hover-card>
</template>

<script setup>
import DateTime from '../date-time.vue';
import FormVersionString from '../form-version/string.vue';
import HoverCard from '../hover-card.vue';

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

defineOptions({
name: 'HoverCardForm'
});

const { form } = useRequestData();
</script>
7 changes: 7 additions & 0 deletions src/components/hover-cards.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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 Popover from './popover.vue';

import useHoverCardResources from '../request-data/hover-card';
Expand All @@ -40,6 +41,12 @@ To add a new type of hover card:
- Check whether the hover card should be used in AuditRow.
*/
const types = {
form: {
component: HoverCardForm,
requests: ({ projectId, xmlFormId }) => ({
form: { url: apiPaths.form(projectId, xmlFormId), extended: true }
})
},
dataset: {
component: HoverCardDataset,
requests: ({ projectId, name }) => ({
Expand Down
12 changes: 10 additions & 2 deletions src/components/link-if-can.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->
<template>
<router-link v-if="canRoute(to)" :to="to" class="link-if-can">
<router-link v-if="canRoute(to)" ref="link" :to="to" class="link-if-can">
<slot></slot>
</router-link>
<span v-else class="link-if-can">
<span v-else ref="span" class="link-if-can">
<slot></slot>
</span>
</template>

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

import useRoutes from '../composables/routes';

defineProps({
Expand All @@ -29,6 +31,12 @@ defineProps({
});

const { canRoute } = useRoutes();

const link = ref(null);
const span = ref(null);
const $el = computed(() => (link.value != null ? link.value.$el : span.value));

defineExpose({ $el });
</script>

<style lang="scss">
Expand Down
5 changes: 4 additions & 1 deletion src/components/project/form-access/row.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ except according to the terms contained in the LICENSE file.
<template v-if="frozen">
<td class="project-form-access-row-form-name">
<span v-if="form.publishedAt == null" class="icon-edit" v-tooltip.sr-only></span>
<router-link :to="primaryFormPath(form)" v-tooltip.text>{{ form.nameOrId }}</router-link>
<form-link :form="form" v-tooltip.text/>
<span v-if="form.publishedAt == null" class="sr-only">&nbsp;{{ $t('draftTitle') }}</span>
</td>
<td>
Expand Down Expand Up @@ -50,11 +50,14 @@ except according to the terms contained in the LICENSE file.
</template>

<script>
import FormLink from '../../form/link.vue';

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

export default {
name: 'ProjectFormAccessRow',
components: { FormLink },
props: {
form: {
type: Object,
Expand Down
12 changes: 7 additions & 5 deletions src/components/project/form-row.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ except according to the terms contained in the LICENSE file.
</td>
<td class="form-name">
<template v-if="canLinkToFormOverview">
<router-link :to="primaryFormPath(form)">{{ form.nameOrId }}</router-link>
<form-link :form="form"/>
</template>
<template v-else-if="canLinkToSubmissions">
<router-link :to="submissionsPath.all">{{ form.nameOrId }}</router-link>
<form-link :form="form" :to="submissionsPath.all"/>
</template>
<template v-else>
<template v-if="canLinkToEnketo">
Expand All @@ -29,6 +29,7 @@ except according to the terms contained in the LICENSE file.
{{ form.nameOrId }}
</template>
</template>

<span v-if="showIdForDuplicateName" class="duplicate-form-id">({{ form.xmlFormId }})</span>
</td>
<template v-if="form.publishedAt != null">
Expand Down Expand Up @@ -93,6 +94,7 @@ except according to the terms contained in the LICENSE file.
import { DateTime } from 'luxon';

import DateTimeComponent from '../date-time.vue';
import FormLink from '../form/link.vue';

import useReviewState from '../../composables/review-state';
import useRoutes from '../../composables/routes';
Expand All @@ -102,7 +104,7 @@ import { useRequestData } from '../../request-data';

export default {
name: 'ProjectFormRow',
components: { DateTime: DateTimeComponent },
components: { DateTime: DateTimeComponent, FormLink },
props: {
form: {
type: Object,
Expand All @@ -122,11 +124,11 @@ export default {
setup() {
const { projects } = useRequestData();
const { duplicateFormNamesPerProject } = projects.toRefs();
const { formPath, primaryFormPath } = useRoutes();
const { formPath } = useRoutes();
const { reviewStateIcon } = useReviewState();
return {
duplicateFormNamesPerProject,
formPath, primaryFormPath,
formPath,
reviewStateIcon
};
},
Expand Down
4 changes: 4 additions & 0 deletions src/request-data/hover-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ 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 { transformForm } from './util';
import { useRequestData } from './index';

export default () => {
const { createResource } = useRequestData();
return {
form: createResource('form', () => ({
transformResponse: ({ data }) => transformForm(data)
})),
dataset: createResource('dataset'),
entity: createResource('entity')
};
Expand Down
7 changes: 5 additions & 2 deletions test/components/audit/table.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RouterLinkStub } from '@vue/test-utils';
import ActorLink from '../../../src/components/actor-link.vue';
import AuditRow from '../../../src/components/audit/row.vue';
import DateTime from '../../../src/components/date-time.vue';
import FormLink from '../../../src/components/form/link.vue';
import DatasetLink from '../../../src/components/dataset/link.vue';
import Selectable from '../../../src/components/selectable.vue';

Expand Down Expand Up @@ -186,12 +187,14 @@ describe('AuditTable', () => {
actor: testData.extendedUsers.first(),
action,
actee: testData.standardForms
.createPast(1, { xmlFormId: 'a b', name: 'My Form' })
.createPast(1, { name: 'My Form' })
.last()
});
const row = mountComponent();
testType(row, type);
await testTarget(row, 'My Form', '/projects/1/forms/a%20b');
await testTarget(row, 'My Form', FormLink, (link) => {
link.props().form.xmlFormId.should.equal('f');
});
});
}

Expand Down
Loading

0 comments on commit 1f040c7

Please sign in to comment.