I have the error message below ,when I try to change the value of a cell of an Agrid table:
Component Error s.forEach is not a function
App running locally
Full text of the error message : Component Error s.forEach is not a function python
Streamlit and Python versions : Streamlit, version 1.38.0 , Python 3.12.4
my script below:
import streamlit as st
from streamlit.components.v1 import components, component_arrow
import pandas as pd
import re
from difflib import SequenceMatcher
from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode, ColumnsAutoSizeMode, JsCode
from io import BytesIO
from typing import List, Dict, Any
from functools import lru_cache
@lru_cache(maxsize=1000)
def normalize_text(text: str) -> str:
return text.strip().lower() if pd.notna(text) else ""
@lru_cache(maxsize=1000)
def similarity(a: str, b: str) -> float:
return SequenceMatcher(None, a, b).ratio()
def is_valid_email(email: str) -> bool:
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(email_regex, email))
def find_similar_contacts(df: pd.DataFrame, email_threshold: float = 0.8, global_threshold: float = 0.8) -> pd.DataFrame:
required_columns = ['ContactID', 'Email', 'BU']
if not all(col in df.columns for col in required_columns):
raise ValueError(f"Le fichier doit contenir au moins les colonnes suivantes : {', '.join(required_columns)}")
df['Email_norm'] = df['Email'].apply(normalize_text)
df['Email valide'] = df['Email'].apply(is_valid_email)
similar_data = []
email_groups = {}
group_counter = 0
cross_bu_group_counter = 0
cross_bu_groups = {}
for i, row in df.iterrows():
email_similar_contacts = set()
unique_similar_emails = set()
email_partial_similar_contacts = set()
global_similar_contacts = set()
global_partial_similar_contacts = set()
bus_with_email = set()
current_email_group = None
current_cross_bu_group = None
for j, other_row in df.iterrows():
if i != j:
email_sim = similarity(row['Email_norm'], other_row['Email_norm'])
if email_sim >= email_threshold:
email_similar_contacts.add(other_row['ContactID'])
unique_similar_emails.add(other_row['Email'])
bus_with_email.add(other_row['BU'])
if email_sim < 1:
email_partial_similar_contacts.add(other_row['ContactID'])
if other_row['Email'] in email_groups:
current_email_group = email_groups[other_row['Email']]
elif current_email_group is None:
current_email_group = group_counter
group_counter += 1
if email_sim == 1:
if other_row['Email'] in cross_bu_groups:
current_cross_bu_group = cross_bu_groups[other_row['Email']]
elif current_cross_bu_group is None:
current_cross_bu_group = cross_bu_group_counter
cross_bu_group_counter += 1
unique_similar_emails.add(row['Email'])
if current_email_group is None:
current_email_group = group_counter
group_counter += 1
if current_cross_bu_group is not None:
for email in unique_similar_emails:
if similarity(email, row['Email']) == 1:
cross_bu_groups[email] = current_cross_bu_group
for email in unique_similar_emails:
email_groups[email] = current_email_group
similar_data.append({
'ContactID': row['ContactID'],
'Email': row['Email'],
'BU': row['BU'],
'Nombre emails similaires': len(email_similar_contacts),
'ContactID emails similaires': ", ".join(email_similar_contacts),
'ContactID emails similaires partiel': ", ".join(email_partial_similar_contacts),
'Email cross BU': ", ".join(bus_with_email),
'Emails identique': bool(email_similar_contacts),
'Emails partiellement similaires': bool(email_partial_similar_contacts),
'Contact cross BU': len(bus_with_email) > 1,
'Email valide': row['Email valide'],
'Emails similaires uniques': ", ".join(unique_similar_emails),
'Arbitrage contacts similaires': "",
'Arbitrage contacts cross BU': "",
'Groupe emails similaires': current_email_group,
'Groupe cross BU': current_cross_bu_group
})
return pd.DataFrame(similar_data)
@st.cache_data
def to_excel(df: pd.DataFrame) -> bytes:
output = BytesIO()
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
df.to_excel(writer, index=False, sheet_name='Sheet1')
return output.getvalue()
def create_grid(df: pd.DataFrame) -> Dict[str, Any]:
gb = GridOptionsBuilder.from_dataframe(df)
gb.configure_side_bar()
gb.configure_default_column(groupable=True, filter=True, editable=False)
return gb.build()
def download_button(df: pd.DataFrame, format: str, filename: str) -> None:
if format == "CSV":
data = df.to_csv(index=False)
mime = 'text/csv'
else:
data = to_excel(df)
mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
st.download_button(
label=f"Télécharger la liste en {format}",
data=data,
file_name=filename,
mime=mime,
)
def function_test():
st.markdown("<h1 style='text-align: center; color: #030F40;'>Référentiel </h1>", unsafe_allow_html=True)
st.markdown("####", unsafe_allow_html=True)
uploaded_file = st.file_uploader("Téléchargez votre fichier Excel", type=["xlsx"])
if uploaded_file is not None:
try:
df = pd.read_excel(uploaded_file)
required_columns = ['ContactID', 'Email', 'BU']
if not all(col in df.columns for col in required_columns):
st.error(f"Le fichier doit contenir au moins les colonnes suivantes : {', '.join(required_columns)}")
return
perfect_duplicates = df[df.duplicated()]
df = df.drop_duplicates()
if not perfect_duplicates.empty:
with st.expander("Doublons parfaits supprimés", expanded=True):
st.info("Les doublons parfaits suivants ont été supprimés automatiquement lors du chargement :")
st.dataframe(perfect_duplicates)
results_df = find_similar_contacts(df)
tabs = st.tabs(["Analyse des données chargées", "Arbitrage des contacts similaires", "Arbitrage des contacts cross BU"])
with tabs[0]:
st.info("Cette section représente les données chargées après analyse:")
columns_to_display = ["ContactID", "Email", "BU", "Email valide", "Emails partiellement similaires", "Contact cross BU"]
AgGrid(
results_df[columns_to_display],
gridOptions=create_grid(results_df[columns_to_display]),
enable_enterprise_modules=True,
update_mode=GridUpdateMode.VALUE_CHANGED,
fit_columns_on_grid_load=True,
theme="streamlit",
columns_auto_size_mode=ColumnsAutoSizeMode.FIT_ALL_COLUMNS_TO_VIEW,
editable=False
)
download_format = st.selectbox("Sélectionnez le format de téléchargement pour 'Analyse des données chargées'", ["CSV", "XLSX"], key="download_analyse")
download_button(results_df, download_format, f'resultats_similarite.{download_format.lower()}')
with tabs[1]:
st.info("Cette section permet l'arbitrage des lignes de contacts similaires:")
columns_similaires = [
"ContactID", "Email", "BU", "Emails partiellement similaires",
"Emails similaires uniques", "Arbitrage contacts similaires", "Groupe emails similaires"
]
df_similaires = results_df[columns_similaires].copy()
grid_options = create_grid(df_similaires)
grid_options['columnDefs'] = [
{
'field': col,
'editable': col == "Arbitrage contacts similaires",
'cellEditor': 'agSelectCellEditor',
'cellEditorParams': {
'values': JsCode("params.data['Emails similaires uniques'].split(', ')")
} if col == "Arbitrage contacts similaires" else None
} for col in df_similaires.columns
]
grid_response = AgGrid(
df_similaires,
gridOptions=grid_options,
enable_enterprise_modules=True,
update_mode=GridUpdateMode.VALUE_CHANGED,
fit_columns_on_grid_load=True,
theme="streamlit",
columns_auto_size_mode=ColumnsAutoSizeMode.FIT_ALL_COLUMNS_TO_VIEW,
allow_unsafe_jscode=True
)
download_format_similaires = st.selectbox("Sélectionnez le format de téléchargement pour 'Arbitrage des contacts similaires'", ["CSV", "XLSX"], key="download_similaires")
download_button(df_similaires, download_format_similaires, f'arbitrage_contacts_similaires.{download_format_similaires.lower()}')
if st.button("Sauvegarder les modifications (Contacts similaires)"):
updated_df = pd.DataFrame(grid_response['data'])
st.success("Les modifications ont été sauvegardées avec succès!")
with tabs[2]:
st.info("Cette section permet l'arbitrage des lignes de contacts cross BU:")
columns_cross_bu = [
"ContactID", "Email", "BU", "Contact cross BU", "Email cross BU", "Arbitrage contacts cross BU", "Groupe cross BU"
]
df_cross_bu = results_df[columns_cross_bu].copy()
grid_options_cross_bu = create_grid(df_cross_bu)
grid_options_cross_bu['columnDefs'] = [
{
'field': col,
'editable': col == "Arbitrage contacts cross BU",
'cellEditor': 'agSelectCellEditor',
'cellEditorParams': {
'values': JsCode("params.data['Email cross BU'].split(', ')")
} if col == "Arbitrage contacts cross BU" else None
} for col in df_cross_bu.columns
]
try:
grid_response_cross_bu = AgGrid(
df_cross_bu,
gridOptions=grid_options_cross_bu,
enable_enterprise_modules=True,
update_mode=GridUpdateMode.VALUE_CHANGED,
fit_columns_on_grid_load=True,
theme="streamlit",
columns_auto_size_mode=ColumnsAutoSizeMode.FIT_ALL_COLUMNS_TO_VIEW,
allow_unsafe_jscode=True
)
except Exception as e:
st.error(f"Une erreur s'est produite lors de l'affichage de la grille : {str(e)}")
download_format_cross_bu = st.selectbox("Sélectionnez le format de téléchargement pour 'Arbitrage des contacts cross BU'", ["CSV", "XLSX"], key="download_cross_bu")
download_button(df_cross_bu, download_format_cross_bu, f'arbitrage_contacts_cross_bu.{download_format_cross_bu.lower()}')
if st.button("Sauvegarder les modifications (Contacts cross BU)"):
updated_df_cross_bu = pd.DataFrame(grid_response_cross_bu['data'])
st.success("Les modifications ont été sauvegardées avec succès!")
except Exception as e:
st.error(f"Une erreur s'est produite lors du traitement du fichier : {str(e)}")
if __name__ == "__main__":
function_test()