Merge pull request #57 from parcelvoy/chore/fix-list-generation-and-twilio

Fix list generation and Twilio
This commit is contained in:
chrishills 2023-02-26 14:10:54 -06:00 committed by GitHub
commit b35288d65e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 54 additions and 20 deletions

View file

@ -0,0 +1,17 @@
exports.up = async function(knex) {
await knex.schema.table('user_list', function(table) {
table.integer('version').after('event_id').defaultTo(0).index()
})
await knex.schema.table('lists', function(table) {
table.integer('version').after('users_count').defaultTo(0)
})
}
exports.down = async function(knex) {
await knex.schema.table('user_list', function(table) {
table.dropColumn('version')
})
await knex.schema.table('lists', function(table) {
table.dropColumn('version')
})
}

View file

@ -24,7 +24,7 @@ export const pagedCampaigns = async (params: SearchParams, projectId: number) =>
params,
['name'],
b => {
b.where({ project_id: projectId })
b.where({ project_id: projectId }).orderBy('id', 'desc')
params?.tags?.length && b.whereIn('id', createTagSubquery(Campaign, projectId, params.tags))
return b
},

View file

@ -25,7 +25,7 @@ export default class TextJob extends Job {
}
// Send and render text
const channel = await loadTextChannel(campaign.project_id, project.id)
const channel = await loadTextChannel(campaign.provider_id, project.id)
if (!channel) {
await updateSendState(campaign, user, 'failed')
return

View file

@ -47,7 +47,7 @@ export default class TwilioTextProvider extends TextProvider {
async send(message: TextMessage): Promise<TextResponse> {
const { to, text } = message
const form = new FormData()
const form = new URLSearchParams()
form.append('From', this.phone_number)
form.append('To', to)
form.append('Body', text)

View file

@ -10,6 +10,7 @@ export default class List extends Model {
type!: ListType
state!: ListState
rule?: Rule
version!: number
users_count?: number
static jsonAttributes = ['rule']
@ -21,6 +22,7 @@ export class UserList extends Model {
user_id!: number
list_id!: number
event_id!: number
version!: number
deleted_at?: Date
static tableName = 'user_list'

View file

@ -21,7 +21,7 @@ export default class ListPopulateJob extends Job {
const list = await getList(listId, projectId) as DynamicList
if (!list) return
await populateList(list.id, list.rule)
await populateList(list, list.rule)
App.main.queue.enqueue(ListStatsJob.from(listId, projectId))
}

View file

@ -56,7 +56,8 @@ export const createList = async (projectId: number, params: ListCreateParams): P
project_id: projectId,
})
if (list.type === 'dynamic') {
const hasRules = (params.rule?.children?.length ?? 0) > 0
if (list.type === 'dynamic' && hasRules) {
App.main.queue.enqueue(
ListPopulateJob.from(list.id, list.project_id),
)
@ -102,19 +103,21 @@ export const importUsersToList = async (list: List, stream: FileStream) => {
await updateList(list.id, { state: 'ready' })
}
export const populateList = async (id: number, rule: Rule) => {
const list = await updateList(id, { state: 'loading' })
export const populateList = async (list: List, rule: Rule) => {
const { id, version: oldVersion } = list
const version = oldVersion + 1
await updateList(id, { state: 'loading', version })
type UserListChunk = { user_id: number, list_id: number }[]
type UserListChunk = { user_id: number, list_id: number, version: number }[]
const insertChunk = async (chunk: UserListChunk) => {
return await UserList.query()
.insert(chunk)
.onConflict(['user_id', 'list_id'])
.ignore()
.merge(['version'])
}
await ruleQuery(rule)
.where('users.project_id', list!.project_id)
.where('users.project_id', list.project_id)
.stream(async function(stream) {
// Stream results and insert in chunks of 100
@ -122,7 +125,7 @@ export const populateList = async (id: number, rule: Rule) => {
let chunk: UserListChunk = []
let i = 0
for await (const { id: user_id } of stream) {
chunk.push({ user_id, list_id: id })
chunk.push({ user_id, list_id: id, version })
i++
if (i % chunkSize === 0) {
await insertChunk(chunk)
@ -133,6 +136,10 @@ export const populateList = async (id: number, rule: Rule) => {
// Insert remaining items
await insertChunk(chunk)
})
// Once list is regenerated, drop any users from previous version
await UserList.delete(qb => qb.where('version', '<', version))
await updateList(id, { state: 'ready' })
}

View file

@ -10,6 +10,7 @@ export const getUserEvents = async (id: number, params: SearchParams, projectId:
params,
['name'],
b => b.where('project_id', projectId)
.where('user_id', id),
.where('user_id', id)
.orderBy('id', 'desc'),
)
}

View file

@ -98,6 +98,7 @@
}
.ui-button.secondary {
background: var(--color-background);
border: 1px solid var(--color-grey);
color: var(--color-primary);
}

View file

@ -64,11 +64,13 @@
}
.project-switcher .switcher-value {
color: var(--color-primary);
font-size: 14px;
font-weight: 500;
}
.project-switcher .project-switcher-icon {
color: var(--color-primary);
width: 1rem;
height: 1rem;
flex-shrink: 0;

View file

@ -97,6 +97,7 @@
border-radius: var(--border-radius-inner);
font-weight: 500;
margin: 3px 0;
gap: 10px;
}
.ui-select .select-option:first-child {
@ -125,12 +126,12 @@
.ui-select .select-option.selected .option-icon {
display: block;
height: 20px;
height: 18px;
}
.ui-select .select-option.selected svg {
height: 20px;
width: 20px;
height: 15px;
width: 15px;
}
.ui-select .select-option.disabled {

View file

@ -69,7 +69,10 @@ export default function Campaigns() {
key: 'delivery',
cell: ({ item }) => `${item.delivery?.sent ?? 0} / ${item.delivery?.total ?? 0}`,
},
{ key: 'launched_at' },
{
key: 'send_at',
title: 'Launched At',
},
{ key: 'updated_at' },
{
key: 'options',

View file

@ -21,7 +21,7 @@ const RuleSection = ({ list, onRuleSave }: { list: DynamicList, onRuleSave: (rul
const [rule, setRule] = useState<WrapperRule>(list.rule)
return <>
<Heading size="h3" title="Rules" actions={
<Button size="small" onClick={() => onRuleSave(rule) }>Save List</Button>
<Button size="small" onClick={() => onRuleSave(rule) }>Save Rules</Button>
} />
<RuleBuilder rule={rule} setRule={setRule} />
</>
@ -86,8 +86,8 @@ export default function ListDetail() {
<Dialog
open={isDialogOpen}
onClose={setIsDialogOpen}
title="List Saved!">
Initial list generation will happen in the background. Please reload the page to see the latest status.
title="Success">
List generation will happen in the background. Please reload the page to see the latest status.
</Dialog>
<Modal

View file

@ -88,7 +88,7 @@ const RuleSet = ({ group, onChange, onDelete }: RuleSetParams) => {
return <div className="rule-set">
{group && <div className="rule-set-header">
{!onDelete && 'Target all users '}
{!onDelete && 'Target users '}
{isEvent
? <>
Did