mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-01 19:30:29 -06:00
Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
607 lines
15 KiB
TypeScript
607 lines
15 KiB
TypeScript
import { createId } from "@paralleldrive/cuid2";
|
|
import {
|
|
TActionMetric,
|
|
TAllOperators,
|
|
TAttributeOperator,
|
|
TBaseFilter,
|
|
TBaseFilters,
|
|
TDeviceOperator,
|
|
TSegment,
|
|
TSegmentActionFilter,
|
|
TSegmentAttributeFilter,
|
|
TSegmentConnector,
|
|
TSegmentDeviceFilter,
|
|
TSegmentFilter,
|
|
TSegmentOperator,
|
|
TSegmentPersonFilter,
|
|
TSegmentSegmentFilter,
|
|
} from "@formbricks/types/segment";
|
|
|
|
// type guard to check if a resource is a filter
|
|
export const isResourceFilter = (resource: TSegmentFilter | TBaseFilters): resource is TSegmentFilter => {
|
|
return (resource as TSegmentFilter).root !== undefined;
|
|
};
|
|
|
|
export const convertOperatorToText = (operator: TAllOperators) => {
|
|
switch (operator) {
|
|
case "equals":
|
|
return "=";
|
|
case "notEquals":
|
|
return "!=";
|
|
case "lessThan":
|
|
return "<";
|
|
case "lessEqual":
|
|
return "<=";
|
|
case "greaterThan":
|
|
return ">";
|
|
case "greaterEqual":
|
|
return ">=";
|
|
case "isSet":
|
|
return "is set";
|
|
case "isNotSet":
|
|
return "is not set";
|
|
case "contains":
|
|
return "contains ";
|
|
case "doesNotContain":
|
|
return "does not contain";
|
|
case "startsWith":
|
|
return "starts with";
|
|
case "endsWith":
|
|
return "ends with";
|
|
case "userIsIn":
|
|
return "User is in";
|
|
case "userIsNotIn":
|
|
return "User is not in";
|
|
default:
|
|
return operator;
|
|
}
|
|
};
|
|
|
|
export const convertOperatorToTitle = (operator: TAllOperators) => {
|
|
switch (operator) {
|
|
case "equals":
|
|
return "Equals";
|
|
case "notEquals":
|
|
return "Not equals to";
|
|
case "lessThan":
|
|
return "Less than";
|
|
case "lessEqual":
|
|
return "Less than or equal to";
|
|
case "greaterThan":
|
|
return "Greater than";
|
|
case "greaterEqual":
|
|
return "Greater than or equal to";
|
|
case "isSet":
|
|
return "Is set";
|
|
case "isNotSet":
|
|
return "Is not set";
|
|
case "contains":
|
|
return "Contains";
|
|
case "doesNotContain":
|
|
return "Does not contain";
|
|
case "startsWith":
|
|
return "Starts with";
|
|
case "endsWith":
|
|
return "Ends with";
|
|
case "userIsIn":
|
|
return "User is in";
|
|
case "userIsNotIn":
|
|
return "User is not in";
|
|
default:
|
|
return operator;
|
|
}
|
|
};
|
|
|
|
export const convertMetricToText = (metric: TActionMetric) => {
|
|
switch (metric) {
|
|
case "lastQuarterCount":
|
|
return "Last quarter (Count)";
|
|
case "lastMonthCount":
|
|
return "Last month (Count)";
|
|
case "lastWeekCount":
|
|
return "Last week (Count)";
|
|
case "occuranceCount":
|
|
return "Occurance (Count)";
|
|
case "lastOccurranceDaysAgo":
|
|
return "Last occurrance (Days ago)";
|
|
case "firstOccurranceDaysAgo":
|
|
return "First occurrance (Days ago)";
|
|
default:
|
|
return metric;
|
|
}
|
|
};
|
|
|
|
export const addFilterBelow = (group: TBaseFilters, resourceId: string, filter: TBaseFilter) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === resourceId) {
|
|
group.splice(i + 1, 0, filter);
|
|
break;
|
|
}
|
|
} else {
|
|
if (group[i].id === resourceId) {
|
|
group.splice(i + 1, 0, filter);
|
|
break;
|
|
} else {
|
|
addFilterBelow(resource, resourceId, filter);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
export const createGroupFromResource = (group: TBaseFilters, resourceId: string) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const filters = group[i];
|
|
if (isResourceFilter(filters.resource)) {
|
|
if (filters.resource.id === resourceId) {
|
|
const newGroupToAdd: TBaseFilter = {
|
|
id: createId(),
|
|
connector: filters.connector,
|
|
resource: [
|
|
{
|
|
...filters,
|
|
connector: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
group.splice(i, 1, newGroupToAdd);
|
|
|
|
break;
|
|
}
|
|
} else {
|
|
if (group[i].id === resourceId) {
|
|
// make an outer group, wrap the current group in it and add a filter below it
|
|
|
|
// const newFilter: TBaseFilter = {
|
|
// id: createId(),
|
|
// connector: "and",
|
|
// resource: {
|
|
// id: createId(),
|
|
// root: { type: "attribute", attributeClassName: "" },
|
|
// qualifier: { operator: "endsWith" },
|
|
// value: "",
|
|
// },
|
|
// };
|
|
|
|
const outerGroup: TBaseFilter = {
|
|
connector: filters.connector,
|
|
id: createId(),
|
|
resource: [{ ...filters, connector: null }],
|
|
};
|
|
|
|
group.splice(i, 1, outerGroup);
|
|
|
|
break;
|
|
} else {
|
|
createGroupFromResource(filters.resource, resourceId);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
export const moveResourceUp = (group: TBaseFilters, i: number) => {
|
|
if (i === 0) {
|
|
return;
|
|
}
|
|
|
|
const previousTemp = group[i - 1];
|
|
|
|
group[i - 1] = group[i];
|
|
group[i] = previousTemp;
|
|
|
|
if (i - 1 === 0) {
|
|
const newConnector = group[i - 1].connector;
|
|
|
|
group[i - 1].connector = null;
|
|
group[i].connector = newConnector;
|
|
}
|
|
};
|
|
|
|
export const moveResourceDown = (group: TBaseFilters, i: number) => {
|
|
if (i === group.length - 1) {
|
|
return;
|
|
}
|
|
|
|
const temp = group[i + 1];
|
|
group[i + 1] = group[i];
|
|
group[i] = temp;
|
|
|
|
// after the swap, determine if the connector should be null or not
|
|
if (i === 0) {
|
|
const nextConnector = group[i].connector;
|
|
|
|
group[i].connector = null;
|
|
group[i + 1].connector = nextConnector;
|
|
}
|
|
};
|
|
|
|
export const moveResource = (group: TBaseFilters, resourceId: string, direction: "up" | "down") => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === resourceId) {
|
|
if (direction === "up") {
|
|
moveResourceUp(group, i);
|
|
break;
|
|
} else {
|
|
moveResourceDown(group, i);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
if (group[i].id === resourceId) {
|
|
if (direction === "up") {
|
|
moveResourceUp(group, i);
|
|
break;
|
|
} else {
|
|
moveResourceDown(group, i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
moveResource(resource, resourceId, direction);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const deleteResource = (group: TBaseFilters, resourceId: string) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource) && resource.id === resourceId) {
|
|
group.splice(i, 1);
|
|
|
|
if (group.length) {
|
|
group[0].connector = null;
|
|
}
|
|
|
|
break;
|
|
} else if (!isResourceFilter(resource) && group[i].id === resourceId) {
|
|
group.splice(i, 1);
|
|
|
|
if (group.length) {
|
|
group[0].connector = null;
|
|
}
|
|
|
|
break;
|
|
} else if (!isResourceFilter(resource)) {
|
|
deleteResource(resource, resourceId);
|
|
}
|
|
}
|
|
|
|
// check and delete all empty groups
|
|
deleteEmptyGroups(group);
|
|
};
|
|
|
|
export const deleteEmptyGroups = (group: TBaseFilters) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (!isResourceFilter(resource) && resource.length === 0) {
|
|
group.splice(i, 1);
|
|
} else if (!isResourceFilter(resource)) {
|
|
deleteEmptyGroups(resource);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const addFilterInGroup = (group: TBaseFilters, groupId: string, filter: TBaseFilter) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
continue;
|
|
} else {
|
|
if (group[i].id === groupId) {
|
|
const { resource } = group[i];
|
|
|
|
if (!isResourceFilter(resource)) {
|
|
if (resource.length === 0) {
|
|
resource.push({
|
|
...filter,
|
|
connector: null,
|
|
});
|
|
} else {
|
|
resource.push(filter);
|
|
}
|
|
}
|
|
|
|
break;
|
|
} else {
|
|
addFilterInGroup(resource, groupId, filter);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
export const toggleGroupConnector = (
|
|
group: TBaseFilters,
|
|
groupId: string,
|
|
newConnectorValue: TSegmentConnector
|
|
) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
if (!isResourceFilter(resource)) {
|
|
if (group[i].id === groupId) {
|
|
group[i].connector = newConnectorValue;
|
|
break;
|
|
} else {
|
|
toggleGroupConnector(resource, groupId, newConnectorValue);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
export const toggleFilterConnector = (
|
|
group: TBaseFilters,
|
|
filterId: string,
|
|
newConnectorValue: TSegmentConnector
|
|
) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
group[i].connector = newConnectorValue;
|
|
}
|
|
} else {
|
|
toggleFilterConnector(resource, filterId, newConnectorValue);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updateOperatorInFilter = (
|
|
group: TBaseFilters,
|
|
filterId: string,
|
|
newOperator: TAttributeOperator | TSegmentOperator | TDeviceOperator
|
|
) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
resource.qualifier.operator = newOperator;
|
|
break;
|
|
}
|
|
} else {
|
|
updateOperatorInFilter(resource, filterId, newOperator);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updateAttributeClassNameInFilter = (
|
|
group: TBaseFilters,
|
|
filterId: string,
|
|
newAttributeClassName: string
|
|
) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
(resource as TSegmentAttributeFilter).root.attributeClassName = newAttributeClassName;
|
|
break;
|
|
}
|
|
} else {
|
|
updateAttributeClassNameInFilter(resource, filterId, newAttributeClassName);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updatePersonIdentifierInFilter = (
|
|
group: TBaseFilters,
|
|
filterId: string,
|
|
newPersonIdentifier: string
|
|
) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
(resource as TSegmentPersonFilter).root.personIdentifier = newPersonIdentifier;
|
|
}
|
|
} else {
|
|
updatePersonIdentifierInFilter(resource, filterId, newPersonIdentifier);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updateActionClassIdInFilter = (
|
|
group: TBaseFilters,
|
|
filterId: string,
|
|
newActionClassId: string
|
|
) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
(resource as TSegmentActionFilter).root.actionClassId = newActionClassId;
|
|
break;
|
|
}
|
|
} else {
|
|
updateActionClassIdInFilter(resource, filterId, newActionClassId);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updateMetricInFilter = (group: TBaseFilters, filterId: string, newMetric: TActionMetric) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
(resource as TSegmentActionFilter).qualifier.metric = newMetric;
|
|
break;
|
|
}
|
|
} else {
|
|
updateMetricInFilter(resource, filterId, newMetric);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updateSegmentIdInFilter = (group: TBaseFilters, filterId: string, newSegmentId: string) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
(resource as TSegmentSegmentFilter).root.segmentId = newSegmentId;
|
|
resource.value = newSegmentId;
|
|
break;
|
|
}
|
|
} else {
|
|
updateSegmentIdInFilter(resource, filterId, newSegmentId);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updateFilterValue = (group: TBaseFilters, filterId: string, newValue: string | number) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
resource.value = newValue;
|
|
|
|
break;
|
|
}
|
|
} else {
|
|
updateFilterValue(resource, filterId, newValue);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updateDeviceTypeInFilter = (
|
|
group: TBaseFilters,
|
|
filterId: string,
|
|
newDeviceType: "phone" | "desktop"
|
|
) => {
|
|
for (let i = 0; i < group.length; i++) {
|
|
const { resource } = group[i];
|
|
|
|
if (isResourceFilter(resource)) {
|
|
if (resource.id === filterId) {
|
|
(resource as TSegmentDeviceFilter).root.deviceType = newDeviceType;
|
|
resource.value = newDeviceType;
|
|
break;
|
|
}
|
|
} else {
|
|
updateDeviceTypeInFilter(resource, filterId, newDeviceType);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const formatSegmentDateFields = (segment: TSegment): TSegment => {
|
|
if (typeof segment.createdAt === "string") {
|
|
segment.createdAt = new Date(segment.createdAt);
|
|
}
|
|
|
|
if (typeof segment.updatedAt === "string") {
|
|
segment.updatedAt = new Date(segment.updatedAt);
|
|
}
|
|
|
|
return segment;
|
|
};
|
|
|
|
export const searchForAttributeClassNameInSegment = (
|
|
filters: TBaseFilters,
|
|
attributeClassName: string
|
|
): boolean => {
|
|
for (let filter of filters) {
|
|
const { resource } = filter;
|
|
|
|
if (isResourceFilter(resource)) {
|
|
const { root } = resource;
|
|
const { type } = root;
|
|
|
|
if (type === "attribute") {
|
|
const { attributeClassName: className } = root;
|
|
if (className === attributeClassName) {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
const found = searchForAttributeClassNameInSegment(resource, attributeClassName);
|
|
if (found) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// check if a segment has a filter with "type" other than "attribute" or "person"
|
|
// if it does, this is an advanced segment
|
|
export const isAdvancedSegment = (filters: TBaseFilters): boolean => {
|
|
for (let filter of filters) {
|
|
const { resource } = filter;
|
|
|
|
if (isResourceFilter(resource)) {
|
|
const { root } = resource;
|
|
const { type } = root;
|
|
|
|
if (type !== "attribute" && type !== "person") {
|
|
return true;
|
|
}
|
|
} else {
|
|
// the resource is a group, so we don't need to recurse, we know that this is an advanced segment
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
type TAttributeFilter = {
|
|
attributeClassName: string;
|
|
operator: TAttributeOperator;
|
|
value: string;
|
|
};
|
|
|
|
export const transformSegmentFiltersToAttributeFilters = (
|
|
filters: TBaseFilters
|
|
): TAttributeFilter[] | null => {
|
|
const attributeFilters: TAttributeFilter[] = [];
|
|
|
|
for (let filter of filters) {
|
|
const { resource } = filter;
|
|
|
|
if (isResourceFilter(resource)) {
|
|
const { root, qualifier, value } = resource;
|
|
const { type } = root;
|
|
|
|
if (type === "attribute") {
|
|
const { attributeClassName } = root;
|
|
const { operator } = qualifier;
|
|
|
|
attributeFilters.push({
|
|
attributeClassName,
|
|
operator: operator as TAttributeOperator,
|
|
value: value.toString(),
|
|
});
|
|
}
|
|
|
|
if (type === "person") {
|
|
const { operator } = qualifier;
|
|
|
|
attributeFilters.push({
|
|
attributeClassName: "userId",
|
|
operator: operator as TAttributeOperator,
|
|
value: value.toString(),
|
|
});
|
|
}
|
|
} else {
|
|
// the resource is a group, so we don't need to recurse, we know that this is an advanced segment
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return attributeFilters;
|
|
};
|