Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Smart webhook filters (optional, additive).** A webhook trigger can now carry an optional set of
pre-dispatch conditions, evaluated per event before delivery: it fires only when **all** conditions
match (AND). Conditions match on `sender` / `recipient` / `body` / `type` / `mentions` / `fromMe` /
`hasMedia` / `isGroup` with `is` / `isNot` / `contains` / `equals` operators;
message-only conditions are skipped for non-message events, so a `*`-subscribed webhook still fires on
session events. A webhook with no filters behaves exactly as before. Contact-id conditions
(`sender`/`recipient`/`mentions`) match by the engine-neutral `WaId` key, so a filter written as a
plain number or in any dialect (`@c.us` / `@s.whatsapp.net` / `@lid`) matches the same person - and a
lid-addressed sender (e.g. an unresolved `@lid` group participant) matches a phone filter once the
persistent `lid -> phone` table knows the mapping. Configurable via the API (`filters` on create/update)
and a new FilterBuilder UI on the dashboard's Webhooks page. (#379)

- **Configurable first-boot init timeout for the whatsapp-web.js engine (`WWEBJS_AUTH_TIMEOUT_MS`).**
On slow first boots (e.g. WSL2 or low-resource containers) the engine's fixed 30s wait for WhatsApp
Web to finish loading could expire before the QR code was generated, aborting startup. Set
Expand Down
4 changes: 2 additions & 2 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

289 changes: 289 additions & 0 deletions dashboard/src/components/FilterBuilder.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
.filter-builder {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.25rem;
}

.filter-builder-head {
display: flex;
flex-direction: column;
gap: 0.125rem;
}

.filter-builder-title {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary, #64748b);
}

.filter-builder-hint {
font-size: 0.75rem;
color: var(--text-secondary);
}

.filter-row {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
gap: 0.5rem;
padding: 0.5rem;
background: var(--bg-light);
border: 1px solid var(--border);
border-radius: 8px;
}

.filter-row select {
height: 2.25rem;
padding: 0 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-white);
color: var(--text-primary);
font-size: 0.8125rem;
}

/* field + operator: fixed width; value flexes to fill the rest */
.filter-row > select {
width: 100%;
}

.filter-field {
flex: 0 0 8.5rem;
}

.filter-operator {
flex: 0 0 7rem;
}

.filter-value {
flex: 1 1 auto;
min-width: 0;
}

.filter-remove {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.25rem;
width: 2.25rem;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}

.filter-remove svg {
width: 16px;
height: 16px;
flex-shrink: 0;
stroke: var(--text-secondary);
}

.filter-remove:hover {
border-color: var(--error);
color: var(--error);
}

.filter-remove:hover svg {
stroke: var(--error);
}

.filter-add {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.625rem;
border: 1px dashed var(--border);
border-radius: 6px;
background: transparent;
color: var(--primary);
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
}

.filter-add:hover {
border-color: var(--primary);
}

/* Text operator */
.filter-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.375rem;
}

/* The global `.modal-body input` (full width + large padding + bg) leaks into the
case-sensitive checkbox; reset it (2-class selector beats `.modal-body input`). */
.filter-case input {
width: auto;
margin: 0;
padding: 0;
}

.filter-text input[type='text'] {
width: 100%;
height: 2.25rem;
padding: 0 0.625rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-white);
color: var(--text-primary);
font-size: 0.8125rem;
}

.filter-case {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 0.75rem;
color: var(--text-secondary);
cursor: pointer;
}

/* Enum multi-select */
.filter-enum {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}

.enum-tag {
font-size: 0.75rem;
padding: 0.3125rem 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-white);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}

.enum-tag:hover {
border-color: var(--primary);
color: var(--primary);
}

.enum-tag.selected {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}

/* Chips input with autocomplete */
.chips-input {
position: relative;
}

.chips-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.375rem;
min-height: 2.25rem;
padding: 0.25rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-white);
}

.chip {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background: rgba(37, 211, 102, 0.15);
border: 1px solid rgba(37, 211, 102, 0.25);
color: var(--primary);
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
max-width: 100%;
}

.chip-remove {
display: inline-flex;
align-items: center;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
padding: 0;
}

/* `.chips-row` prefix raises specificity above the global `.modal-body input`, which
would otherwise force width:100% (dropping the field onto its own line) + bg + padding. */
.chips-row .chips-text {
flex: 1 1 6rem;
min-width: 6rem;
width: auto;
height: 26px;
padding: 2px 8px;
border: none;
outline: none;
background: var(--bg-light);
color: var(--text-primary);
font-size: 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
}

/* keep the yes/no select compact rather than filling the value column */
.filter-bool {
width: auto;
}

.chips-suggestions {
position: absolute;
z-index: 20;
top: calc(100% + 2px);
left: 0;
right: 0;
max-height: 14rem;
overflow-y: auto;
background: var(--bg-white);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
}

.chips-suggestion {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
padding: 0.5rem 0.625rem;
border: none;
background: transparent;
cursor: pointer;
text-align: left;
}

.chips-suggestion:hover {
background: var(--bg-light);
}

.chips-suggestion-name {
font-size: 0.8125rem;
color: var(--text-primary);
}

.chips-suggestion-id {
font-size: 0.6875rem;
color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace;
}

.chips-suggestion-add {
color: var(--primary);
font-weight: 600;
font-size: 0.8125rem;
border-top: 1px solid var(--border);
}
Loading
Loading