mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 03:29:03 +00:00
* Setup system provided templates for ChangeEvent notifications * Setup handlebars helpers for system-default notification templates
This commit is contained in:
parent
8deab28071
commit
dacd60d535
@ -0,0 +1,23 @@
|
||||
package org.openmetadata.service.template.handlebars;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
|
||||
/**
|
||||
* Interface for all Handlebars helpers. Each helper should implement this interface to provide a
|
||||
* consistent way of registering helpers with Handlebars.
|
||||
*/
|
||||
public interface HandlebarsHelper {
|
||||
/**
|
||||
* Get the name of this helper as it will be used in templates.
|
||||
*
|
||||
* @return The helper name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Register this helper with the given Handlebars instance.
|
||||
*
|
||||
* @param handlebars The Handlebars instance to register with
|
||||
*/
|
||||
void register(Handlebars handlebars);
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package org.openmetadata.service.template.handlebars;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import java.util.List;
|
||||
import org.openmetadata.service.template.handlebars.helpers.BuildEntityUrlHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.DiffHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.EndsWithHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.EqHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.FormatColumnValueHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.FormatDateHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.GroupEventChangesHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.JoinListHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.OrHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.ResolveDomainHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.SplitHelper;
|
||||
import org.openmetadata.service.template.handlebars.helpers.StartsWithHelper;
|
||||
|
||||
/**
|
||||
* Provider for Handlebars instances configured with all custom helpers for notification templates.
|
||||
*/
|
||||
public class HandlebarsProvider {
|
||||
|
||||
private static final Handlebars INSTANCE = createInstance();
|
||||
|
||||
private HandlebarsProvider() {
|
||||
/* Private constructor for singleton */
|
||||
}
|
||||
|
||||
public static Handlebars getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static Handlebars createInstance() {
|
||||
Handlebars handlebars = new Handlebars();
|
||||
|
||||
for (HandlebarsHelper helper : getAllHelpers()) {
|
||||
helper.register(handlebars);
|
||||
}
|
||||
|
||||
return handlebars;
|
||||
}
|
||||
|
||||
private static List<HandlebarsHelper> getAllHelpers() {
|
||||
return List.of(
|
||||
new JoinListHelper(),
|
||||
new StartsWithHelper(),
|
||||
new EndsWithHelper(),
|
||||
new EqHelper(),
|
||||
new OrHelper(),
|
||||
new DiffHelper(),
|
||||
new ResolveDomainHelper(),
|
||||
new FormatDateHelper(),
|
||||
new FormatColumnValueHelper(),
|
||||
new GroupEventChangesHelper(),
|
||||
new SplitHelper(),
|
||||
new BuildEntityUrlHelper());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import okhttp3.HttpUrl;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
import org.openmetadata.service.util.email.EmailUtil;
|
||||
|
||||
public class BuildEntityUrlHelper implements HandlebarsHelper {
|
||||
|
||||
private static final String KEY_TYPE = "type";
|
||||
private static final String KEY_FQN = "fullyQualifiedName";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "buildEntityUrl";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(entityRef, options) -> {
|
||||
if (!(entityRef instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> refMap = (Map<String, Object>) entityRef;
|
||||
|
||||
Optional<String> typeOpt =
|
||||
Optional.ofNullable(refMap.get(KEY_TYPE))
|
||||
.map(Object::toString)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty());
|
||||
|
||||
Optional<String> fqnOpt =
|
||||
Optional.ofNullable(refMap.get(KEY_FQN))
|
||||
.map(Object::toString)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty());
|
||||
|
||||
if (typeOpt.isEmpty() || fqnOpt.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String baseUrl = EmailUtil.getOMBaseURL();
|
||||
if (baseUrl == null || baseUrl.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpUrl base = HttpUrl.parse(baseUrl);
|
||||
if (base == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return base.newBuilder()
|
||||
.addPathSegment(typeOpt.get())
|
||||
.addPathSegment(fqnOpt.get())
|
||||
.build()
|
||||
.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import com.github.jknack.handlebars.Options;
|
||||
import java.util.LinkedList;
|
||||
import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to generate inline diff with add/remove markers. Usage: {{diff oldValue newValue}}
|
||||
*
|
||||
* <p>Shows differences like: "This is <s>old</s> <b>new</b> text"
|
||||
*/
|
||||
public class DiffHelper implements HandlebarsHelper {
|
||||
|
||||
private static final String INSERT_OPEN = "<b>";
|
||||
private static final String INSERT_CLOSE = "</b> ";
|
||||
private static final String DELETE_OPEN = "<s>";
|
||||
private static final String DELETE_CLOSE = "</s> ";
|
||||
private static final String SPACE = " ";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "diff";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
String oldValue = context != null ? context.toString() : "";
|
||||
String newValue = extractNewValue(options);
|
||||
|
||||
return generateDiff(oldValue, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the new value from Handlebars options parameters.
|
||||
*
|
||||
* @param options Handlebars options containing parameters
|
||||
* @return The new value string, or empty string if not present
|
||||
*/
|
||||
private String extractNewValue(Options options) {
|
||||
if (options.params.length > 0 && options.param(0) != null) {
|
||||
return options.param(0).toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an inline diff between old and new values using DiffMatchPatch.
|
||||
* Inserts are marked with <b> tags, deletions with <s> tags.
|
||||
*
|
||||
* @param oldValue The original text
|
||||
* @param newValue The new text
|
||||
* @return HTML formatted diff string
|
||||
*/
|
||||
private String generateDiff(String oldValue, String newValue) {
|
||||
DiffMatchPatch diffEngine = new DiffMatchPatch();
|
||||
LinkedList<DiffMatchPatch.Diff> diffs = diffEngine.diffMain(oldValue, newValue);
|
||||
diffEngine.diffCleanupSemantic(diffs);
|
||||
|
||||
StringBuilder diffResult = new StringBuilder();
|
||||
|
||||
for (DiffMatchPatch.Diff diff : diffs) {
|
||||
String trimmedText = diff.text.trim();
|
||||
|
||||
if (trimmedText.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String formattedDiff =
|
||||
switch (diff.operation) {
|
||||
case EQUAL -> trimmedText + SPACE;
|
||||
case INSERT -> INSERT_OPEN + trimmedText + INSERT_CLOSE;
|
||||
case DELETE -> DELETE_OPEN + trimmedText + DELETE_CLOSE;
|
||||
};
|
||||
|
||||
diffResult.append(formattedDiff);
|
||||
}
|
||||
|
||||
return diffResult.toString().trim();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import com.github.jknack.handlebars.Helper;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to check if a string ends with a given suffix.
|
||||
* Usage: {{#if (endsWith name '.tags')}}...{{/if}}
|
||||
*/
|
||||
public class EndsWithHelper implements HandlebarsHelper {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "endsWith";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(Helper<String>)
|
||||
(text, options) -> {
|
||||
if (text == null || options.params.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object suffixParam = options.param(0);
|
||||
if (suffixParam == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return text.endsWith(suffixParam.toString());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import java.util.Objects;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to check if two values are equal.
|
||||
* Usage: {{#if (eq value1 value2)}}...{{/if}}
|
||||
*/
|
||||
public class EqHelper implements HandlebarsHelper {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "eq";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
if (context == null || options.params.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object firstParam = options.param(0);
|
||||
|
||||
// Compare the context with the first parameter
|
||||
return Objects.equals(context, firstParam);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import java.util.Map;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to format column values based on their type. Usage: {{formatColumnValue columnData}}
|
||||
*
|
||||
* <p>Formats column definitions as: columnName (type, constraints)
|
||||
*/
|
||||
public class FormatColumnValueHelper implements HandlebarsHelper {
|
||||
|
||||
private static final String EMPTY_VALUE = "<em>empty</em>";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "formatColumnValue";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
if (context == null) {
|
||||
return EMPTY_VALUE;
|
||||
}
|
||||
|
||||
return switch (context) {
|
||||
case Map<?, ?> columnMap -> formatColumnData(columnMap);
|
||||
default -> context.toString();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats column data into a readable string representation.
|
||||
* Format: columnName (dataType, constraint) or just columnName if no metadata.
|
||||
*
|
||||
* @param columnData Map containing column metadata
|
||||
* @return Formatted column string
|
||||
*/
|
||||
private String formatColumnData(Map<?, ?> columnData) {
|
||||
Object nameValue = columnData.get("name");
|
||||
Object dataTypeValue = columnData.get("dataType");
|
||||
Object constraintValue = columnData.get("constraint");
|
||||
|
||||
String columnName = nameValue != null ? nameValue.toString() : "";
|
||||
|
||||
if (dataTypeValue == null) {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
StringBuilder formattedColumn = new StringBuilder(columnName);
|
||||
formattedColumn.append(" (").append(dataTypeValue);
|
||||
|
||||
if (constraintValue != null) {
|
||||
formattedColumn.append(", ").append(constraintValue);
|
||||
}
|
||||
|
||||
formattedColumn.append(")");
|
||||
return formattedColumn.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Helper to format dates in a readable format. Usage: {{formatDate timestamp}}
|
||||
*
|
||||
* <p>Handles Long timestamps, Date objects, and String timestamps.
|
||||
*/
|
||||
public class FormatDateHelper implements HandlebarsHelper {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FormatDateHelper.class);
|
||||
private static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat(DATE_FORMAT_PATTERN));
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "formatDate";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
if (context == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
Date dateToFormat = parseToDate(context);
|
||||
if (dateToFormat != null) {
|
||||
return DATE_FORMATTER.get().format(dateToFormat);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Error formatting date for value '{}': {}", context, e.getMessage());
|
||||
}
|
||||
|
||||
return context.toString();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses various input types to a Date object.
|
||||
* Supports Long timestamps, Date objects, and String representations of timestamps.
|
||||
*
|
||||
* @param input The input object to parse
|
||||
* @return Date object if parsing successful, null otherwise
|
||||
*/
|
||||
private Date parseToDate(Object input) {
|
||||
return switch (input) {
|
||||
case Long timestamp -> new Date(timestamp);
|
||||
case Date date -> date;
|
||||
case String stringValue -> parseStringToDate(stringValue);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse a string as a timestamp.
|
||||
*
|
||||
* @param stringValue The string to parse
|
||||
* @return Date object if string is a valid timestamp, null otherwise
|
||||
*/
|
||||
private Date parseStringToDate(String stringValue) {
|
||||
try {
|
||||
long timestamp = Long.parseLong(stringValue);
|
||||
return new Date(timestamp);
|
||||
} catch (NumberFormatException e) {
|
||||
// Not a timestamp string, return null to fall back to string representation
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,204 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import com.github.jknack.handlebars.Helper;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.openmetadata.schema.type.ChangeDescription;
|
||||
import org.openmetadata.schema.type.FieldChange;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to group change events by merging adds/deletes into updates where applicable.
|
||||
* Matches FormatterUtil.getFormattedMessages() grouping.
|
||||
*
|
||||
* Template usage:
|
||||
* {{#with (groupEventChanges event.changeDescription) as |changes|}}
|
||||
* {{#if changes.updates}}...{{/if}}
|
||||
* {{#if changes.adds}}...{{/if}}
|
||||
* {{#if changes.deletes}}...{{/if}}
|
||||
* {{/with}}
|
||||
*/
|
||||
public class GroupEventChangesHelper implements HandlebarsHelper {
|
||||
|
||||
private record Indexed<T>(T value, int index) {}
|
||||
|
||||
private record MatchResult(List<FieldChange> mergedUpdates, BitSet matchedDeleteIdx) {}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "groupEventChanges";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(), (Helper<ChangeDescription>) (changeDesc, options) -> groupChanges(changeDesc));
|
||||
}
|
||||
|
||||
private ChangeGroups groupChanges(ChangeDescription changeDesc) {
|
||||
// Extract all field changes from the change description
|
||||
final List<FieldChange> fieldsUpdated = extractFieldsUpdated(changeDesc);
|
||||
final List<FieldChange> fieldsAdded = extractFieldsAdded(changeDesc);
|
||||
final List<FieldChange> fieldsDeleted = extractFieldsDeleted(changeDesc);
|
||||
|
||||
// Build the initial update list from direct updates
|
||||
final List<FieldChange> updates = new ArrayList<>(buildDirectUpdates(fieldsUpdated));
|
||||
|
||||
// Build indexed queues for efficient lookup of adds by field name
|
||||
final Map<String, Deque<Indexed<FieldChange>>> addedQueues = buildAddedQueues(fieldsAdded);
|
||||
final BitSet consumedAddedIdx = new BitSet(fieldsAdded.size());
|
||||
|
||||
// Match deletes with adds to create merged updates
|
||||
final MatchResult matchResult =
|
||||
matchDeletesWithAdds(fieldsDeleted, addedQueues, consumedAddedIdx);
|
||||
|
||||
// Collect unmatched deletes
|
||||
final List<FieldChange> unmatchedDeletes =
|
||||
collectUnmatchedDeletes(fieldsDeleted, matchResult.matchedDeleteIdx());
|
||||
|
||||
// Collect remaining adds that weren't consumed
|
||||
final List<FieldChange> remainingAdds = collectRemainingAdds(fieldsAdded, consumedAddedIdx);
|
||||
|
||||
// Combine all updates (direct + merged)
|
||||
updates.addAll(matchResult.mergedUpdates());
|
||||
|
||||
return new ChangeGroups(updates, remainingAdds, unmatchedDeletes);
|
||||
}
|
||||
|
||||
private List<FieldChange> buildDirectUpdates(List<FieldChange> fieldsUpdated) {
|
||||
return fieldsUpdated.stream()
|
||||
.map(
|
||||
u ->
|
||||
new FieldChange()
|
||||
.withName(u.getName())
|
||||
.withOldValue(u.getOldValue())
|
||||
.withNewValue(u.getNewValue()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private MatchResult matchDeletesWithAdds(
|
||||
List<FieldChange> fieldsDeleted,
|
||||
Map<String, Deque<Indexed<FieldChange>>> addedQueues,
|
||||
BitSet consumedAddedIdx) {
|
||||
|
||||
final List<FieldChange> mergedUpdates = new ArrayList<>();
|
||||
final BitSet matchedDeleteIdx = new BitSet(fieldsDeleted.size());
|
||||
|
||||
for (int i = 0; i < fieldsDeleted.size(); i++) {
|
||||
final FieldChange delete = fieldsDeleted.get(i);
|
||||
final Deque<Indexed<FieldChange>> queue = addedQueues.get(delete.getName());
|
||||
if (queue != null && !queue.isEmpty()) {
|
||||
// Found matching add - consume it and create merged update
|
||||
final Indexed<FieldChange> consumed = queue.removeFirst();
|
||||
final FieldChange matchedAdd = consumed.value();
|
||||
consumedAddedIdx.set(consumed.index());
|
||||
|
||||
// Mark all duplicate adds as consumed
|
||||
markDuplicatesAsConsumed(queue, matchedAdd, consumedAddedIdx);
|
||||
|
||||
// Create merged update
|
||||
mergedUpdates.add(
|
||||
new FieldChange()
|
||||
.withName(delete.getName())
|
||||
.withOldValue(delete.getOldValue())
|
||||
.withNewValue(matchedAdd.getNewValue()));
|
||||
|
||||
matchedDeleteIdx.set(i); // Remember this delete was matched
|
||||
}
|
||||
}
|
||||
|
||||
return new MatchResult(mergedUpdates, matchedDeleteIdx);
|
||||
}
|
||||
|
||||
private List<FieldChange> collectUnmatchedDeletes(
|
||||
List<FieldChange> fieldsDeleted, BitSet matchedDeleteIdx) {
|
||||
|
||||
final List<FieldChange> unmatchedDeletes = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < fieldsDeleted.size(); i++) {
|
||||
if (!matchedDeleteIdx.get(i)) {
|
||||
final FieldChange delete = fieldsDeleted.get(i);
|
||||
unmatchedDeletes.add(
|
||||
new FieldChange()
|
||||
.withName(delete.getName())
|
||||
.withOldValue(delete.getOldValue())
|
||||
.withNewValue(null));
|
||||
}
|
||||
}
|
||||
|
||||
return unmatchedDeletes;
|
||||
}
|
||||
|
||||
private List<FieldChange> collectRemainingAdds(
|
||||
List<FieldChange> fieldsAdded, BitSet consumedAddedIdx) {
|
||||
|
||||
final List<FieldChange> remainingAdds = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < fieldsAdded.size(); i++) {
|
||||
if (!consumedAddedIdx.get(i)) {
|
||||
final FieldChange add = fieldsAdded.get(i);
|
||||
remainingAdds.add(
|
||||
new FieldChange()
|
||||
.withName(add.getName())
|
||||
.withOldValue(null)
|
||||
.withNewValue(add.getNewValue()));
|
||||
}
|
||||
}
|
||||
|
||||
return remainingAdds;
|
||||
}
|
||||
|
||||
private void markDuplicatesAsConsumed(
|
||||
Deque<Indexed<FieldChange>> queue, FieldChange matchedAdd, BitSet consumedAddedIdx) {
|
||||
|
||||
queue.removeIf(
|
||||
ix -> {
|
||||
if (Objects.equals(ix.value(), matchedAdd)) {
|
||||
consumedAddedIdx.set(ix.index());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private List<FieldChange> extractFieldsUpdated(ChangeDescription changeDesc) {
|
||||
return Optional.ofNullable(changeDesc)
|
||||
.map(ChangeDescription::getFieldsUpdated)
|
||||
.orElse(List.of());
|
||||
}
|
||||
|
||||
private List<FieldChange> extractFieldsAdded(ChangeDescription changeDesc) {
|
||||
return Optional.ofNullable(changeDesc).map(ChangeDescription::getFieldsAdded).orElse(List.of());
|
||||
}
|
||||
|
||||
private List<FieldChange> extractFieldsDeleted(ChangeDescription changeDesc) {
|
||||
return Optional.ofNullable(changeDesc)
|
||||
.map(ChangeDescription::getFieldsDeleted)
|
||||
.orElse(List.of());
|
||||
}
|
||||
|
||||
private Map<String, Deque<Indexed<FieldChange>>> buildAddedQueues(List<FieldChange> fieldsAdded) {
|
||||
final Map<String, Deque<Indexed<FieldChange>>> addedQueues = new LinkedHashMap<>();
|
||||
for (int i = 0; i < fieldsAdded.size(); i++) {
|
||||
final FieldChange fc = fieldsAdded.get(i);
|
||||
addedQueues
|
||||
.computeIfAbsent(fc.getName(), k -> new ArrayDeque<>())
|
||||
.addLast(new Indexed<>(fc, i));
|
||||
}
|
||||
return addedQueues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for grouped changes that the template can access
|
||||
*/
|
||||
public record ChangeGroups(
|
||||
List<FieldChange> updates, List<FieldChange> adds, List<FieldChange> deletes) {}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.openmetadata.schema.type.TagLabel;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to join lists into comma-separated strings. Usage: {{joinList tags}}
|
||||
*
|
||||
* <p>Handles TagLabel objects specially by extracting their FQN.
|
||||
*/
|
||||
public class JoinListHelper implements HandlebarsHelper {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "joinList";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
if (context == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (context instanceof Collection<?> collection) {
|
||||
return collection.stream()
|
||||
.map(this::extractDisplayValue)
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
return context.toString();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the display value from an item based on its type.
|
||||
* For TagLabel objects, returns the tag FQN.
|
||||
* For Maps with tagFQN field, returns that value.
|
||||
* Otherwise returns the string representation.
|
||||
*/
|
||||
private String extractDisplayValue(Object item) {
|
||||
return switch (item) {
|
||||
case TagLabel tagLabel -> {
|
||||
String fqn = tagLabel.getTagFQN();
|
||||
yield fqn != null ? fqn : "";
|
||||
}
|
||||
case Map<?, ?> map -> {
|
||||
Object tagFQN = map.get("tagFQN");
|
||||
yield tagFQN != null ? tagFQN.toString() : item.toString();
|
||||
}
|
||||
case null -> "";
|
||||
default -> item.toString();
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to check if any of the values are truthy.
|
||||
* Usage: {{#if (or value1 value2 value3)}}...{{/if}}
|
||||
*/
|
||||
public class OrHelper implements HandlebarsHelper {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "or";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
// Check if context is truthy
|
||||
if (isTruthy(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any parameter is truthy
|
||||
if (options.params != null) {
|
||||
for (Object param : options.params) {
|
||||
if (isTruthy(param)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isTruthy(Object value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
if (value instanceof String) {
|
||||
return !((String) value).isEmpty();
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue() != 0;
|
||||
}
|
||||
// Non-null objects are considered truthy
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.openmetadata.schema.entity.domains.Domain;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Helper to resolve domain references to display names. Usage: {{resolveDomain domainRef}}
|
||||
*
|
||||
* <p>Handles both EntityReference objects and UUID strings.
|
||||
*/
|
||||
public class ResolveDomainHelper implements HandlebarsHelper {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResolveDomainHelper.class);
|
||||
private static final String NO_DOMAIN = "(no domain)";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "resolveDomain";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
if (context == null) {
|
||||
return NO_DOMAIN;
|
||||
}
|
||||
|
||||
return switch (context) {
|
||||
case Map<?, ?> entityReference -> {
|
||||
String resolved = resolveFromEntityReference(entityReference);
|
||||
yield resolved != null ? resolved : context.toString();
|
||||
}
|
||||
case String domainId -> resolveFromString(domainId);
|
||||
default -> context.toString();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves domain name from an EntityReference map.
|
||||
* Prioritizes displayName, then name, then fullyQualifiedName.
|
||||
*
|
||||
* @param entityReference Map containing entity reference fields
|
||||
* @return Resolved domain name or null if not found
|
||||
*/
|
||||
private String resolveFromEntityReference(Map<?, ?> entityReference) {
|
||||
Object displayName = entityReference.get("displayName");
|
||||
Object name = entityReference.get("name");
|
||||
Object fullyQualifiedName = entityReference.get("fullyQualifiedName");
|
||||
|
||||
if (displayName != null && !displayName.toString().isEmpty()) {
|
||||
return displayName.toString();
|
||||
}
|
||||
|
||||
if (name != null && !name.toString().isEmpty()) {
|
||||
return name.toString();
|
||||
}
|
||||
|
||||
if (fullyQualifiedName != null) {
|
||||
return fullyQualifiedName.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves domain from a string UUID.
|
||||
* Attempts to fetch the domain entity and return its fully qualified name.
|
||||
*
|
||||
* @param domainId String representation of domain UUID
|
||||
* @return Domain's fully qualified name or the original ID if resolution fails
|
||||
*/
|
||||
private String resolveFromString(String domainId) {
|
||||
try {
|
||||
Domain domain =
|
||||
Entity.getEntity(Entity.DOMAIN, UUID.fromString(domainId), "id", Include.NON_DELETED);
|
||||
return domain.getFullyQualifiedName();
|
||||
} catch (Exception e) {
|
||||
LOG.debug("Could not resolve domain ID {}: {}", domainId, e.getMessage());
|
||||
return domainId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import java.util.regex.Pattern;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to split a string by a delimiter.
|
||||
* Usage: {{#with (split fieldName '.')}}{{.[1]}}{{/with}}
|
||||
*/
|
||||
public class SplitHelper implements HandlebarsHelper {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "split";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
if (context == null || options.params.length == 0) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
String textToSplit = context.toString();
|
||||
Object delimiterParam = options.param(0);
|
||||
|
||||
if (delimiterParam == null) {
|
||||
return new String[] {textToSplit};
|
||||
}
|
||||
|
||||
String delimiter = delimiterParam.toString();
|
||||
return textToSplit.split(Pattern.quote(delimiter));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package org.openmetadata.service.template.handlebars.helpers;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import org.openmetadata.service.template.handlebars.HandlebarsHelper;
|
||||
|
||||
/**
|
||||
* Helper to check if a string starts with a prefix.
|
||||
* Usage: {{#if (startsWith name 'columns.')}}...{{/if}}
|
||||
*/
|
||||
public class StartsWithHelper implements HandlebarsHelper {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "startsWith";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Handlebars handlebars) {
|
||||
handlebars.registerHelper(
|
||||
getName(),
|
||||
(context, options) -> {
|
||||
if (context == null || options.params.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object firstParam = options.param(0);
|
||||
if (firstParam == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String textToCheck = context.toString();
|
||||
String prefix = firstParam.toString();
|
||||
|
||||
return textToCheck.startsWith(prefix);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-entity-created",
|
||||
"fullyQualifiedName": "system-notification-entity-created",
|
||||
"displayName": "Entity Created Notification",
|
||||
"description": "System default template for entity creation notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "{{entity.entityType}} {{entity.name}} created",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}[{{publisherName}}] <b>{{event.userName}}</b> created {{entity.entityType}} <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a>{{#if entity.description}}<br/>Description: {{entity.description}}{{/if}}{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-entity-default",
|
||||
"fullyQualifiedName": "system-notification-entity-default",
|
||||
"displayName": "Default Entity Notification",
|
||||
"description": "System default fallback template for any unhandled eventTypes",
|
||||
"provider": "system",
|
||||
"templateSubject": "{{event.eventType}} - {{entity.fullyQualifiedName}}",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}[{{publisherName}}] <b>{{event.userName}}</b> performed {{event.eventType}} on <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a>{{#if changes.fieldsUpdated}}<br/><b>Changed:</b> {{#each changes.fieldsUpdated}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}{{/if}}{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-entity-deleted",
|
||||
"fullyQualifiedName": "system-notification-entity-deleted",
|
||||
"displayName": "Entity Deleted Notification",
|
||||
"description": "System default template for entity permanent deletion notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "{{entity.entityType}} {{entity.name}} deleted",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}[{{publisherName}}] <b>{{event.userName}}</b> deleted {{entity.entityType}} {{entity.fullyQualifiedName}}{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-entity-soft-deleted",
|
||||
"fullyQualifiedName": "system-notification-entity-soft-deleted",
|
||||
"displayName": "Entity Soft Deleted Notification",
|
||||
"description": "System default template for entity soft deletion notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "{{entity.entityType}} {{entity.name}} soft deleted",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}[{{publisherName}}] <b>{{event.userName}}</b> soft deleted {{entity.entityType}} <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a>{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-logical-test-case-added",
|
||||
"fullyQualifiedName": "system-notification-logical-test-case-added",
|
||||
"displayName": "Logical Test Case Added Notification",
|
||||
"description": "System default template for logical test case addition notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "Test cases added to {{entity.fullyQualifiedName}}",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}[{{publisherName}}] <b>{{event.userName}}</b> added logical test cases to <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a>{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-post-created",
|
||||
"fullyQualifiedName": "system-notification-post-created",
|
||||
"displayName": "Post Created Notification",
|
||||
"description": "System default template for new post/reply notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "New reply in {{thread.type}} - {{entity.fullyQualifiedName}}",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}{{#if (eq thread.type 'Task')}}[{{publisherName}}] <b>@{{thread.createdBy}}</b> posted a message on the Task with Id: {{thread.task.id}} for Asset <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a>{{#each thread.posts}}<br/><b>@{{from}}:</b> {{message}}{{/each}}{{else if (eq thread.type 'Announcement')}}[{{publisherName}}] <b>@{{thread.createdBy}}</b> posted a message on Announcement{{#each thread.posts}}<br/><b>@{{from}}:</b> {{message}}{{/each}}{{else}}[{{publisherName}}] <b>@{{thread.createdBy}}</b> posted a message on asset <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a>{{#each thread.posts}}<br/><b>@{{from}}:</b> {{message}}{{/each}}{{/if}}{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-task-closed",
|
||||
"fullyQualifiedName": "system-notification-task-closed",
|
||||
"displayName": "Task Closed Notification",
|
||||
"description": "System default template for task closure notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "Task closed - {{entity.fullyQualifiedName}}",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}[{{publisherName}}] <b>@{{thread.createdBy}}</b> closed Task with Id: {{thread.task.id}} for Asset <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a><br/><b>Current Status:</b> Closed{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-task-resolved",
|
||||
"fullyQualifiedName": "system-notification-task-resolved",
|
||||
"displayName": "Task Resolved Notification",
|
||||
"description": "System default template for task resolution notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "Task resolved - {{entity.fullyQualifiedName}}",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}[{{publisherName}}] <b>@{{thread.createdBy}}</b> resolved Task with Id: {{thread.task.id}} for Asset <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a><br/><b>Current Status:</b> Resolved{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-test-result",
|
||||
"fullyQualifiedName": "system-notification-test-result",
|
||||
"displayName": "Test Result Notification",
|
||||
"description": "System default template for test case execution result notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "Test {{testResult.testCaseStatus}} - {{entity.fullyQualifiedName}}",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}<b>Test Result Update</b><br/>Test Case: <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a><br/><b>Status:</b> {{#if (eq testResult.testCaseStatus 'Success')}}<span style=\"color:#48CA9E\">✓ Passed</span>{{else if (eq testResult.testCaseStatus 'Failed')}}<span style=\"color:#F24822\">✗ Failed</span>{{else if (eq testResult.testCaseStatus 'Aborted')}}<span style=\"color:#FFBE0E\">⚠ Aborted</span>{{else}}<span style=\"color:#959595\">⏳ {{testResult.testCaseStatus}}</span>{{/if}}{{#if testResult.result}}<br/><b>Result:</b> {{testResult.result}}{{/if}}{{#if testResult.testResultValue}}<br/><b>Value:</b> {{testResult.testResultValue}}{{/if}}<br/><b>Timestamp:</b> {{formatDate testResult.timestamp}}{{#if entity.description}}<br/><b>Test Description:</b> {{entity.description}}{{/if}}{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-thread-created",
|
||||
"fullyQualifiedName": "system-notification-thread-created",
|
||||
"displayName": "Thread Created Notification",
|
||||
"description": "System default template for new conversation/task/announcement creation notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "New {{thread.type}} - {{entity.fullyQualifiedName}}",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}{{#if (eq thread.type 'Conversation')}}[{{publisherName}}] <b>@{{thread.createdBy}}</b> started a conversation for asset <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a><br/>{{thread.message}}{{else if (eq thread.type 'Task')}}[{{publisherName}}] <b>@{{thread.createdBy}}</b> created a Task for {{entity.entityType}} <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a><br/><b>Task Type:</b> {{thread.task.type}}<br/><b>Assignees:</b> {{#each thread.task.assignees}}@{{name}}{{#unless @last}}, {{/unless}}{{/each}}<br/><b>Current Status:</b> {{thread.task.status}}{{else if (eq thread.type 'Announcement')}}[{{publisherName}}] <b>@{{thread.createdBy}}</b> posted an Announcement<br/><b>Description:</b> {{thread.announcement.description}}<br/><b>Started At:</b> {{formatDate thread.announcement.startTime}}<br/><b>Ends At:</b> {{formatDate thread.announcement.endTime}}{{/if}}{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "system-notification-thread-updated",
|
||||
"fullyQualifiedName": "system-notification-thread-updated",
|
||||
"displayName": "Thread Updated Notification",
|
||||
"description": "System default template for thread update notifications",
|
||||
"provider": "system",
|
||||
"templateSubject": "{{thread.type}} updated - {{entity.fullyQualifiedName}}",
|
||||
"templateBody": "{{#if (eq channel 'email')}}Hello {{recipientName}},<br/><br/>{{/if}}{{#if (eq thread.type 'Task')}}[{{publisherName}}] <b>@{{thread.updatedBy}}</b> posted update on the Task with Id: {{thread.task.id}} for Asset <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a><br/><b>Task Type:</b> {{thread.task.type}}<br/><b>Assignees:</b> {{#each thread.task.assignees}}@{{name}}{{#unless @last}}, {{/unless}}{{/each}}<br/><b>Current Status:</b> {{thread.task.status}}{{else if (eq thread.type 'Announcement')}}[{{publisherName}}] <b>@{{thread.updatedBy}}</b> posted an update on Announcement{{#if (eq event.eventType 'ENTITY_DELETED')}}<br/><b>Announcement Deleted:</b> {{thread.announcement.description}}{{else}}<br/><b>Description:</b> {{thread.announcement.description}}<br/><b>Started At:</b> {{formatDate thread.announcement.startTime}}<br/><b>Ends At:</b> {{formatDate thread.announcement.endTime}}{{/if}}{{else}}[{{publisherName}}] <b>@{{thread.updatedBy}}</b> posted update on Conversation for asset <a href=\"{{entity.href}}\">{{entity.fullyQualifiedName}}</a><br/>{{thread.message}}{{/if}}{{#if (eq channel 'email')}}<br/><br/>Happy Exploring!<br/>Thanks.{{/if}}"
|
||||
}
|
||||
2
pom.xml
2
pom.xml
@ -117,7 +117,7 @@
|
||||
<log4j.version>2.21.0</log4j.version>
|
||||
<org.junit.jupiter.version>5.9.3</org.junit.jupiter.version>
|
||||
<dropwizard-health.version>4.0.14</dropwizard-health.version>
|
||||
<handlebars.version>4.3.1</handlebars.version>
|
||||
<handlebars.version>4.5.0</handlebars.version>
|
||||
<fernet.version>1.5.0</fernet.version>
|
||||
<antlr.version>4.13.2</antlr.version>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user