Skip to content

Commit

Permalink
Feature: Grant Notes feed #3428 (#3532)
Browse files Browse the repository at this point in the history
* feat: add API to retrieve grant notes for specific user via Finder API

- Implemented `getOrganizationNotesForGrantByUser()` function in `grantsCollaboration/notes.js`
- Renamed `getOrganizationNotesForGrant()` to `getCurrentNoteRevisions()` and updated signature
- Made conditional `WHERE` clause for `grantId` and `userId` in `getCurrentNoteRevisions()`
- Created API route `GET /:grantId/notes/user/:userId` in `grants.js` for fetching user-specific grant notes
- Added validation for `afterRevision` and `limit` query parameters in the new API
- Ensured results are paginated and ordered by `created_at` in descending order
- Added necessary imports/exports in `grantsCollaboration/index.js`

* ES Lint fixes

* ES Lint fixes

* ES Lint fixes

* ES Lint fixes

* ES Lint fixes

* first pass

* add notes for user

* adjust styling

* refine grant notes

* add FE tests

* adjust grant notes

* adjust styling

* adjust notes display

* adjust styling

* fix test

* extract header text to component

* adjust styling

* correct limit

* remove unused

* adjust form validation

* use cursor naming

* include limit feature flag

* limit unbounded row query

---------

Co-authored-by: Sushil Rajeeva Bhandary <[email protected]>
  • Loading branch information
greg-adams and sushilrajeeva authored Oct 2, 2024
1 parent 7140173 commit 5e71973
Show file tree
Hide file tree
Showing 24 changed files with 1,006 additions and 274 deletions.
23 changes: 14 additions & 9 deletions packages/client/src/components/CopyButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
:variant="copySuccessTimeout === null ? '' : 'success'"
/>
<b-tooltip
v-if="$refs.container"
v-if="mounted"
ref="tooltip"
:target="$refs.container"
triggers=""
:show="copySuccessTimeout !== null"
triggers="manual"
boundary="window"
>
<b-icon
icon="check-circle-fill"
Expand All @@ -41,21 +42,25 @@ export default {
data() {
return {
copySuccessTimeout: null,
mounted: false,
};
},
mounted() {
this.mounted = true;
},
methods: {
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.$refs.tooltip.$emit('open');
// Show the success indicator
// (Clear previous timeout to ensure multiple clicks in quick succession don't cause issues)
clearTimeout(this.copySuccessTimeout);
this.copySuccessTimeout = setTimeout(
() => {
this.copySuccessTimeout = null;
},
1000,
);
this.copySuccessTimeout = setTimeout(() => {
this.$refs.tooltip.$emit('close');
this.copySuccessTimeout = null;
}, 1000);
},
},
};
Expand Down
25 changes: 15 additions & 10 deletions packages/client/src/components/GrantActivity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<b-card
header-bg-variant="white"
footer-bg-variant="white"
footer-class="p-0"
>
<template #header>
<h3 class="my-2">
Expand All @@ -23,7 +24,6 @@
block
size="lg"
:variant="followBtnVariant"
class="mb-4"
data-follow-btn
:disabled="!followStateLoaded"
@click="toggleFollowState"
Expand All @@ -38,16 +38,17 @@
{{ followBtnLabel }}
</span>
</b-button>
<div>
<div
v-if="grantHasFollowers || showNotesSummary"
class="mt-4"
>
<b-link
v-if="grantHasFollowers"
:class="followSummaryClass"
data-follow-summary
@click="$bvModal.show('grant-followers-modal')"
>
{{ followSummaryText }}
</b-link>

<span
v-if="grantHasFollowers && showNotesSummary"
class="mx-1"
Expand All @@ -58,6 +59,7 @@

<template #footer>
<!-- Feed -->
<GrantNotes @noteSaved="fetchFollowAndNotes" />
</template>
</b-card>

Expand All @@ -72,10 +74,12 @@

<script>
import { mapActions, mapGetters } from 'vuex';
import GrantNotes from '@/components/GrantNotes.vue';
import GrantFollowersModal from '@/components/Modals/GrantFollowers.vue';
export default {
components: {
GrantNotes,
GrantFollowersModal,
},
data() {
Expand All @@ -97,9 +101,6 @@ export default {
followBtnVariant() {
return this.userIsFollowing ? 'success' : 'primary';
},
followSummaryClass() {
return this.followStateLoaded ? 'visible' : 'invisible';
},
followSummaryText() {
const userIsFollower = this.userIsFollowing;
const firstFollowerName = userIsFollower ? 'you' : this.followers[0].user.name;
Expand Down Expand Up @@ -131,12 +132,11 @@ export default {
textCount = '50+';
}
return `${textCount} notes`;
return `${textCount} ${this.notes.length === 1 ? 'note' : 'notes'}`;
},
},
async beforeMount() {
this.fetchFollowState();
this.fetchAllNotes();
this.fetchFollowAndNotes();
},
methods: {
...mapActions({
Expand All @@ -146,6 +146,11 @@ export default {
followGrantForCurrentUser: 'grants/followGrantForCurrentUser',
unfollowGrantForCurrentUser: 'grants/unfollowGrantForCurrentUser',
}),
fetchFollowAndNotes() {
this.followStateLoaded = false;
this.fetchFollowState();
this.fetchAllNotes();
},
async fetchFollowState() {
const followCalls = [
this.getFollowerForGrant({ grantId: this.currentGrant.grant_id }),
Expand Down
37 changes: 37 additions & 0 deletions packages/client/src/components/GrantNote.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import GrantNote from '@/components/GrantNote.vue';
import { id } from '@/helpers/testHelpers';

const note = {
id: id(),
createdAt: new Date().toISOString(),
isRevised: true,
text: 'Text',
grant: { id: id() },
user: {
id: id(),
name: 'User',
email: 'email@net',
avatarColor: 'red',
team: {
id: id(),
name: 'Team',
},
organization: {
id: id(),
name: 'Org',
},
},
};

describe('GrantNote component', () => {
it('renders', () => {
const wrapper = mount(GrantNote, {
props: {
note,
},
});
expect(wrapper.exists()).toBe(true);
});
});
123 changes: 123 additions & 0 deletions packages/client/src/components/GrantNote.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<template>
<div class="d-flex note-container">
<div class="d-flex flex-column">
<UserAvatar
:user-name="note.user.name"
size="2.5rem"
:color="note.user.avatarColor"
/>
<div class="note_vertical position-relative flex-grow-1" />
</div>

<div class="d-flex flex-column flex-grow-1 has-flexi-truncate ml-2">
<UserHeaderText
:name="note.user.name"
:team="note.user.team.name"
/>
<div class="text-gray-500">
<CopyButton
:copy-text="note.user.email"
hide-icon
>
{{ note.user.email }}
</CopyButton>
</div>
<div class="mt-1 text-gray-600">
{{ note.text }}
</div>
<div class="d-flex mt-1 align-items-end">
<span class="note-date-text">
{{ timeElapsedString }}
<span
v-if="note.isRevised"
class="text-gray-500"
>(edited)</span>
</span>
<div class="ml-auto">
<slot name="actions" />
</div>
</div>
</div>
</div>
</template>

<script>
import { DateTime } from 'luxon';
import UserAvatar from '@/components/UserAvatar.vue';
import CopyButton from '@/components/CopyButton.vue';
import UserHeaderText from '@/components/UserHeaderText.vue';
export const formatActivityDate = (createdAtISO) => {
const createdDate = DateTime.fromISO(createdAtISO);
const today = DateTime.now().endOf('day');
let dateText = '';
if (createdDate.hasSame(today, 'day')) {
dateText = createdDate.toRelativeCalendar({ base: today, unit: 'days' });
} else if (createdDate > today.minus({ days: 7 })) {
dateText = createdDate.toRelative({ base: today, unit: 'days' });
} else {
dateText = createdDate.toFormat('MMMM d');
}
return dateText;
};
export default {
components: {
UserAvatar,
CopyButton,
UserHeaderText,
},
props: {
note: {
type: Object,
required: true,
},
},
computed: {
timeElapsedString() {
return formatActivityDate(this.note.createdAt);
},
},
};
</script>

<style lang="scss" scoped>
@import '@/scss/colors-semantic-tokens.scss';
@import '@/scss/colors-base-tokens.scss';
.note-container {
padding: 1rem 1.25rem;
}
.text-gray-500 {
color: $raw-gray-500
}
.note-date-text {
font-size:0.75rem;
}
.note-date-text:first-letter {
text-transform: capitalize;
}
.text-gray-600 {
color: $raw-gray-600;
font-weight: 400;
}
.note_vertical:before {
content: "";
background: $raw-gray-600;
height: calc(100% - .5rem);
width: 1px;
position: absolute;
left: calc(2.5rem / 2);
top: 0.375rem;
bottom: 0;
}
</style>
Loading

0 comments on commit 5e71973

Please sign in to comment.