Customers reported that the CSAT survey was recording their rating the instant they tapped a star — leaving no chance to correct an accidental pick. This change lets the customer freely change their selection until they actually submit the comment/feedback. The rating still saves on click (so we don't lose ratings when a customer never types a comment), but it stays editable until the comment form is submitted. Once that happens, the rating locks. The flow on both surfaces: - Customer taps a star/emoji → rating is saved. - Customer taps a different star/emoji → previous save is overwritten with the new value. - Customer types a comment and submits → latest rating + comment are saved together. - After that submit, the rating is locked and can't be changed. Two surfaces are updated: - **Standalone survey page** (`/survey/responses/:uuid`) — the rating buttons remain re-tappable until the Feedback form is submitted; once submitted, both rating and feedback lock. - **In-conversation widget CSAT** — same behavior, the inline arrow-submit button on the feedback form is the locking action. In-flight guards prevent a race where the customer changes their pick mid-network-call (raised by the codex review on the earlier revision): while a save is in flight, the rating controls are temporarily disabled so the request and the displayed selection can't diverge. ## Closes - https://linear.app/chatwoot/issue/CW-7061/csat-star-rating-submits-on-first-click-needs-confirmation-step ## How to test **Standalone survey page** 1. Enable CSAT on any inbox (Settings → Inboxes → Configuration → CSAT survey). 2. Resolve a conversation in that inbox so a CSAT message is generated. 3. Open the survey URL: `{FRONTEND_URL}/survey/responses/{conversation.uuid}` (easiest: `bundle exec rails runner 'puts Conversation.joins(:messages).where(messages: { content_type: "input_csat" }).last.csat_survey_link'`). 4. Tap a star/emoji — confirm the rating saves (Network panel shows a PUT to `/public/api/v1/csat_survey/{uuid}`). 5. Tap a different star/emoji — confirm a second PUT goes out with the new value; the latest selection is reflected. 6. Type a comment and hit Submit feedback — confirm rating + feedback persist; both controls now lock. 7. Reload the page — the locked state is rehydrated correctly. **Widget CSAT** 1. From an inbox with CSAT enabled, resolve a conversation that has an active widget session. 2. In the widget, the CSAT card appears with stars/emojis + the inline comment box. 3. Tap a star/emoji — confirm a PATCH goes out and the selection visibly updates. 4. Tap different stars/emojis — confirm each overrides the previous save. 5. Type a comment and click the arrow — rating + comment submit together; stars lock. Both display types (emoji and 5-star) should behave consistently. ## What changed - `app/javascript/survey/views/Response.vue` — `selectRating()` saves on every tap and short-circuits while a save is in flight (or after feedback was submitted). Rating components are disabled by `isFeedbackSubmitted || isUpdating` so the lock follows the feedback submission, not the first rating tap. - `app/javascript/survey/components/Rating.vue` — new `isDisabled` prop. The disabled / hover styling and click guard key off it instead of the presence of `selectedRating`, so emojis stay re-clickable until the feedback step locks them. - `app/javascript/shared/components/CustomerSatisfaction.vue` — same shape for the widget: rating click auto-submits and re-clicks override the previous save; controls disabled while a submit is in flight; emoji-button styling and the inline `StarRating` lock on `isFeedbackSubmitted || isUpdating`. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
73 lines
1.3 KiB
Vue
73 lines
1.3 KiB
Vue
<script>
|
|
import { CSAT_RATINGS } from 'shared/constants/messages';
|
|
|
|
export default {
|
|
props: {
|
|
selectedRating: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
isDisabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
emits: ['selectRating'],
|
|
data() {
|
|
return {
|
|
ratings: CSAT_RATINGS,
|
|
};
|
|
},
|
|
|
|
methods: {
|
|
buttonClass(rating) {
|
|
return [
|
|
{ selected: rating.value === this.selectedRating },
|
|
{ disabled: this.isDisabled },
|
|
{ hover: this.isDisabled },
|
|
'emoji-button shadow-none text-3xl lg:text-4xl outline-none mr-8',
|
|
];
|
|
},
|
|
onClick(rating) {
|
|
if (this.isDisabled) return;
|
|
this.$emit('selectRating', rating.value);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="customer-satisfcation mb-2">
|
|
<div class="ratings flex py-5 px-0">
|
|
<button
|
|
v-for="rating in ratings"
|
|
:key="rating.key"
|
|
:class="buttonClass(rating)"
|
|
@click="onClick(rating)"
|
|
>
|
|
{{ rating.emoji }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.emoji-button {
|
|
filter: grayscale(100%);
|
|
&.selected,
|
|
&:hover,
|
|
&:focus,
|
|
&:active {
|
|
filter: grayscale(0%);
|
|
transform: scale(1.32);
|
|
transition: transform 300ms;
|
|
}
|
|
|
|
&.disabled {
|
|
cursor: default;
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
</style>
|