diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-08 21:10:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-08 21:10:23 +0300 |
commit | 8a03b5424b73679d852fbf43781d9422c83869f7 (patch) | |
tree | a24a9c56fce9530492d60a640572922dd65d1072 /app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue | |
parent | 0ebbf19f2d2b87e1f2aca1c59efde1aa6a766cf6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue b/app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue new file mode 100644 index 00000000000..121c3bd94ef --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue @@ -0,0 +1,175 @@ +<script> +import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; +import getUserCalloutsQuery from '~/graphql_shared/queries/get_user_callouts.query.graphql'; + +/** + * A renderless component for querying/dismissing UserCallouts via GraphQL. + * + * Simplest example usage: + * + * <user-callout-dismisser feature-name="my_user_callout"> + * <template #default="{ dismiss, shouldShowCallout }"> + * <my-callout-component + * v-if="shouldShowCallout" + * @close="dismiss" + * /> + * </template> + * </user-callout-dismisser> + * + * If you don't want the asynchronous query to run when the component is + * created, and know by some other means whether the user callout has already + * been dismissed, you can use the `skipQuery` prop, and a regular `v-if` + * directive: + * + * <user-callout-dismisser + * v-if="userCalloutIsNotDismissed" + * feature-name="my_user_callout" + * skip-query + * > + * <template #default="{ dismiss, shouldShowCallout }"> + * <my-callout-component + * v-if="shouldShowCallout" + * @close="dismiss" + * /> + * </template> + * </user-callout-dismisser> + * + * The component exposes various scoped slot props on the default slot, + * allowing for granular rendering behaviors based on the state of the initial + * query and user-initiated mutation: + * + * - dismiss: Function + * - Triggers mutation to dismiss the user callout. + * - isAnonUser: boolean + * - Whether the current user is anonymous or not (i.e., whether or not + * they're logged in). + * - isDismissed: boolean + * - Whether the given user callout has been dismissed or not. + * - isLoadingMutation: boolean + * - Whether the mutation is loading. + * - isLoadingQuery: boolean + * - Whether the initial query is loading. + * - mutationError: string[] | null + * - The mutation's errors, if any; otherwise `null`. + * - queryError: Error | null + * - The query's error, if any; otherwise `null`. + * - shouldShowCallout: boolean + * - A combination of the above which should cover 95% of use cases: `true` + * if the query has loaded without error, and the user is logged in, and + * the callout has not been dismissed yet; `false` otherwise. + */ +export default { + name: 'UserCalloutDismisser', + props: { + featureName: { + type: String, + required: true, + }, + skipQuery: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + currentUser: null, + isDismissedLocal: false, + isLoadingMutation: false, + mutationError: null, + queryError: null, + }; + }, + apollo: { + currentUser: { + query: getUserCalloutsQuery, + update(data) { + return data?.currentUser; + }, + error(err) { + this.queryError = err; + }, + skip() { + return this.skipQuery; + }, + }, + }, + computed: { + featureNameEnumValue() { + return this.featureName.toUpperCase(); + }, + isLoadingQuery() { + return this.$apollo.queries.currentUser.loading; + }, + isAnonUser() { + return !(this.skipQuery || this.queryError || this.isLoadingQuery || this.currentUser); + }, + isDismissedRemote() { + const callouts = this.currentUser?.callouts?.nodes ?? []; + + return callouts.some(({ featureName }) => featureName === this.featureNameEnumValue); + }, + isDismissed() { + return this.isDismissedLocal || this.isDismissedRemote; + }, + slotProps() { + const { + dismiss, + isAnonUser, + isDismissed, + isLoadingMutation, + isLoadingQuery, + mutationError, + queryError, + shouldShowCallout, + } = this; + + return { + dismiss, + isAnonUser, + isDismissed, + isLoadingMutation, + isLoadingQuery, + mutationError, + queryError, + shouldShowCallout, + }; + }, + shouldShowCallout() { + return !(this.isLoadingQuery || this.isDismissed || this.queryError || this.isAnonUser); + }, + }, + methods: { + async dismiss() { + this.isLoadingMutation = true; + this.isDismissedLocal = true; + + try { + const { data } = await this.$apollo.mutate({ + mutation: dismissUserCalloutMutation, + variables: { + input: { + featureName: this.featureName, + }, + }, + }); + + const errors = data?.userCalloutCreate?.errors ?? []; + if (errors.length > 0) { + this.onDismissalError(errors); + } + } catch (err) { + this.onDismissalError([err.message]); + } finally { + this.isLoadingMutation = false; + } + }, + onDismissalError(errors) { + this.mutationError = errors; + }, + }, + render() { + return this.$scopedSlots.default(this.slotProps); + }, +}; +</script> |