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; };