feat: Allow wildcard URL in the campaigns (#6056)
This commit is contained in:
parent
6200559123
commit
823c836906
8 changed files with 104 additions and 19 deletions
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue