feat: Allow wildcard URL in the campaigns (#6056)

This commit is contained in:
Pranav Raj S 2022-12-09 16:43:09 -08:00 committed by GitHub
parent 6200559123
commit 823c836906
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 104 additions and 19 deletions

View file

@ -171,11 +171,12 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { required, url, minLength } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import campaignMixin from 'shared/mixins/campaignMixin'; import campaignMixin from 'shared/mixins/campaignMixin';
import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue'; import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue';
import { URLPattern } from 'urlpattern-polyfill';
export default { export default {
components: { components: {
@ -221,8 +222,23 @@ export default {
}, },
endPoint: { endPoint: {
required, required,
minLength: minLength(7), shouldBeAValidURLPattern(value) {
url, try {
// eslint-disable-next-line
new URLPattern(value);
return true;
} catch (error) {
return false;
}
},
shouldStartWithHTTP(value) {
if (value) {
return (
value.startsWith('https://') || value.startsWith('http://')
);
}
return false;
},
}, },
timeOnPage: { timeOnPage: {
required, required,

View file

@ -30,7 +30,7 @@
<label :class="{ error: $v.selectedInbox.$error }"> <label :class="{ error: $v.selectedInbox.$error }">
{{ $t('CAMPAIGN.ADD.FORM.INBOX.LABEL') }} {{ $t('CAMPAIGN.ADD.FORM.INBOX.LABEL') }}
<select v-model="selectedInbox" @change="onChangeInbox($event)"> <select v-model="selectedInbox" @change="onChangeInbox($event)">
<option v-for="item in inboxes" :key="item.name" :value="item.id"> <option v-for="item in inboxes" :key="item.id" :value="item.id">
{{ item.name }} {{ item.name }}
</option> </option>
</select> </select>
@ -111,10 +111,12 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { required, url, minLength } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import campaignMixin from 'shared/mixins/campaignMixin'; import campaignMixin from 'shared/mixins/campaignMixin';
import { URLPattern } from 'urlpattern-polyfill';
export default { export default {
components: { components: {
WootMessageEditor, WootMessageEditor,
@ -152,8 +154,21 @@ export default {
}, },
endPoint: { endPoint: {
required, required,
minLength: minLength(7), shouldBeAValidURLPattern(value) {
url, try {
// eslint-disable-next-line
new URLPattern(value);
return true;
} catch (error) {
return false;
}
},
shouldStartWithHTTP(value) {
if (value) {
return value.startsWith('https://') || value.startsWith('http://');
}
return false;
},
}, },
timeOnPage: { timeOnPage: {
required, required,

View file

@ -14,6 +14,12 @@
<div v-if="error" class="text-red-400 mt-2 text-xs font-medium"> <div v-if="error" class="text-red-400 mt-2 text-xs font-medium">
{{ error }} {{ error }}
</div> </div>
<div
v-if="!error && helpText"
class="text-red-400 mt-2 text-xs font-medium"
>
{{ helpText }}
</div>
</label> </label>
</template> </template>
<script> <script>
@ -41,6 +47,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
helpText: {
type: String,
default: '',
},
}, },
computed: { computed: {
labelClass() { labelClass() {

View file

@ -1,5 +1,19 @@
export const stripTrailingSlash = ({ URL }) => { import { URLPattern } from 'urlpattern-polyfill';
return URL.replace(/\/$/, '');
export const isPatternMatchingWithURL = (urlPattern, url) => {
let updatedUrlPattern = urlPattern;
const locationObj = new URL(url);
if (updatedUrlPattern.endsWith('/')) {
updatedUrlPattern = updatedUrlPattern.slice(0, -1) + '*\\?*\\#*';
}
if (locationObj.pathname.endsWith('/')) {
locationObj.pathname = locationObj.pathname.slice(0, -1);
}
const pattern = new URLPattern(updatedUrlPattern);
return pattern.test(locationObj.toString());
}; };
// Format all campaigns // Format all campaigns
@ -22,10 +36,7 @@ export const filterCampaigns = ({
isInBusinessHours, isInBusinessHours,
}) => { }) => {
return campaigns.filter(campaign => { return campaigns.filter(campaign => {
const hasMatchingURL = if (!isPatternMatchingWithURL(campaign.url, currentURL)) {
stripTrailingSlash({ URL: campaign.url }) ===
stripTrailingSlash({ URL: currentURL });
if (!hasMatchingURL) {
return false; return false;
} }
if (campaign.triggerOnlyDuringBusinessHours) { if (campaign.triggerOnlyDuringBusinessHours) {

View file

@ -1,7 +1,7 @@
import { import {
stripTrailingSlash,
formatCampaigns, formatCampaigns,
filterCampaigns, filterCampaigns,
isPatternMatchingWithURL,
} from '../campaignHelper'; } from '../campaignHelper';
import campaigns from './campaignFixtures'; import campaigns from './campaignFixtures';
@ -9,11 +9,35 @@ global.chatwootWebChannel = {
workingHoursEnabled: false, workingHoursEnabled: false,
}; };
describe('#Campaigns Helper', () => { describe('#Campaigns Helper', () => {
describe('stripTrailingSlash', () => { describe('#isPatternMatchingWithURL', () => {
it('should return striped trailing slash if url with trailing slash is passed', () => { it('returns correct value if a valid URL is passed', () => {
expect( expect(
stripTrailingSlash({ URL: 'https://www.chatwoot.com/pricing/' }) isPatternMatchingWithURL(
).toBe('https://www.chatwoot.com/pricing'); 'https://chatwoot.com/pricing*',
'https://chatwoot.com/pricing/'
)
).toBe(true);
expect(
isPatternMatchingWithURL(
'https://*.chatwoot.com/pricing/',
'https://app.chatwoot.com/pricing/'
)
).toBe(true);
expect(
isPatternMatchingWithURL(
'https://{*.}?chatwoot.com/pricing?test=true',
'https://app.chatwoot.com/pricing/?test=true'
)
).toBe(true);
expect(
isPatternMatchingWithURL(
'https://{*.}?chatwoot.com/pricing*\\?*',
'https://chatwoot.com/pricing/?test=true'
)
).toBe(true);
}); });
}); });

View file

@ -86,7 +86,8 @@ class Campaign < ApplicationRecord
def validate_url def validate_url
return unless trigger_rules['url'] return unless trigger_rules['url']
errors.add(:url, 'invalid') if inbox.inbox_type == 'Website' && !url_valid?(trigger_rules['url']) use_http_protocol = trigger_rules['url'].starts_with?('http://') || trigger_rules['url'].starts_with?('https://')
errors.add(:url, 'invalid') if inbox.inbox_type == 'Website' && !use_http_protocol
end end
def prevent_completed_campaign_from_update def prevent_completed_campaign_from_update

View file

@ -55,6 +55,7 @@
"tailwindcss": "^1.9.6", "tailwindcss": "^1.9.6",
"turbolinks": "^5.2.0", "turbolinks": "^5.2.0",
"url-loader": "^2.0.0", "url-loader": "^2.0.0",
"urlpattern-polyfill": "^6.0.2",
"v-tooltip": "~2.1.3", "v-tooltip": "~2.1.3",
"videojs-record": "^4.5.0", "videojs-record": "^4.5.0",
"vue": "2.6.12", "vue": "2.6.12",

View file

@ -16381,6 +16381,13 @@ url@^0.11.0:
punycode "1.3.2" punycode "1.3.2"
querystring "0.2.0" querystring "0.2.0"
urlpattern-polyfill@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz#a193fe773459865a2a5c93b246bb794b13d07256"
integrity sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==
dependencies:
braces "^3.0.2"
use@^3.1.0: use@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"