mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-25 07:07:32 -04:00
chore: move responsibility of url valdiation to front-frontend (#5129)
* chore: move responsibility of url valdiation to frontend * chore: fix test * chore: fix tauri build
This commit is contained in:
parent
8947a89a24
commit
e9e483291e
9 changed files with 34 additions and 254 deletions
|
@ -34,12 +34,7 @@ export interface CheckboxCell extends Cell {
|
||||||
|
|
||||||
export interface UrlCell extends Cell {
|
export interface UrlCell extends Cell {
|
||||||
fieldType: FieldType.URL;
|
fieldType: FieldType.URL;
|
||||||
data: UrlCellData;
|
data: string;
|
||||||
}
|
|
||||||
|
|
||||||
export interface UrlCellData {
|
|
||||||
url: string;
|
|
||||||
content?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectCell extends Cell {
|
export interface SelectCell extends Cell {
|
||||||
|
@ -126,10 +121,9 @@ export const pbToSelectCellData = (pb: SelectOptionCellDataPB): SelectCellData =
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const pbToURLCellData = (pb: URLCellDataPB): UrlCellData => ({
|
const pbToURLCellData = (pb: URLCellDataPB): string => (
|
||||||
url: pb.url,
|
pb.content
|
||||||
content: pb.content,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
export const pbToChecklistCellData = (pb: ChecklistCellDataPB): ChecklistCellData => ({
|
export const pbToChecklistCellData = (pb: ChecklistCellDataPB): ChecklistCellData => ({
|
||||||
selectedOptions: pb.selected_options.map(({ id }) => id),
|
selectedOptions: pb.selected_options.map(({ id }) => id),
|
||||||
|
|
|
@ -16,7 +16,7 @@ function UrlCell({ field, cell, placeholder }: Props) {
|
||||||
const cellRef = useRef<HTMLDivElement>(null);
|
const cellRef = useRef<HTMLDivElement>(null);
|
||||||
const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell);
|
const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell);
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setValue(cell.data.content || '');
|
setValue(cell.data);
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
}, [cell, setEditing, setValue]);
|
}, [cell, setEditing, setValue]);
|
||||||
|
|
||||||
|
@ -32,19 +32,17 @@ function UrlCell({ field, cell, placeholder }: Props) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
const str = cell.data.content;
|
if (cell.data) {
|
||||||
|
|
||||||
if (str) {
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openUrl(str);
|
openUrl(cell.data);
|
||||||
}}
|
}}
|
||||||
target={'_blank'}
|
target={'_blank'}
|
||||||
className={'cursor-pointer text-content-blue-400 underline'}
|
className={'cursor-pointer text-content-blue-400 underline'}
|
||||||
>
|
>
|
||||||
{str}
|
{cell.data}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,6 @@ use flowy_derive::ProtoBuf;
|
||||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||||
pub struct URLCellDataPB {
|
pub struct URLCellDataPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub url: String,
|
|
||||||
|
|
||||||
#[pb(index = 2)]
|
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use bytes::Bytes;
|
|
||||||
use collab::core::any_map::AnyMapExtension;
|
use collab::core::any_map::AnyMapExtension;
|
||||||
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
|
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
|
||||||
use collab_database::rows::{new_cell_builder, Cell};
|
use collab_database::rows::{new_cell_builder, Cell};
|
||||||
|
@ -9,9 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
|
|
||||||
use crate::entities::{FieldType, TextFilterPB};
|
use crate::entities::{FieldType, TextFilterPB};
|
||||||
use crate::services::cell::{
|
use crate::services::cell::{stringify_cell, CellDataChangeset, CellDataDecoder};
|
||||||
stringify_cell, CellDataChangeset, CellDataDecoder, CellProtobufBlobParser,
|
|
||||||
};
|
|
||||||
use crate::services::field::type_options::util::ProtobufStr;
|
use crate::services::field::type_options::util::ProtobufStr;
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
|
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
|
||||||
|
@ -146,39 +143,6 @@ impl TypeOptionCellDataCompare for RichTextTypeOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TextCellData(pub String);
|
|
||||||
impl AsRef<str> for TextCellData {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for TextCellData {
|
|
||||||
type Target = String;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for TextCellData {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
self.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TextCellDataParser();
|
|
||||||
impl CellProtobufBlobParser for TextCellDataParser {
|
|
||||||
type Object = TextCellData;
|
|
||||||
fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> {
|
|
||||||
match String::from_utf8(bytes.to_vec()) {
|
|
||||||
Ok(s) => Ok(TextCellData(s)),
|
|
||||||
Err(_) => Ok(TextCellData("".to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct StrCellData(pub String);
|
pub struct StrCellData(pub String);
|
||||||
impl std::ops::Deref for StrCellData {
|
impl std::ops::Deref for StrCellData {
|
||||||
|
|
|
@ -7,161 +7,26 @@ mod tests {
|
||||||
use crate::services::field::FieldBuilder;
|
use crate::services::field::FieldBuilder;
|
||||||
use crate::services::field::URLTypeOption;
|
use crate::services::field::URLTypeOption;
|
||||||
|
|
||||||
/// The expected_str will equal to the input string, but the expected_url will be empty if there's no
|
|
||||||
/// http url in the input string.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn url_type_option_does_not_contain_url_test() {
|
fn url_test() {
|
||||||
let type_option = URLTypeOption::default();
|
|
||||||
let field_type = FieldType::URL;
|
|
||||||
let field = FieldBuilder::from_field_type(field_type).build();
|
|
||||||
assert_url(&type_option, "123", "123", "", &field);
|
|
||||||
assert_url(&type_option, "", "", "", &field);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The expected_str will equal to the input string, but the expected_url will not be empty
|
|
||||||
/// if there's a http url in the input string.
|
|
||||||
#[test]
|
|
||||||
fn url_type_option_contains_url_test() {
|
|
||||||
let type_option = URLTypeOption::default();
|
let type_option = URLTypeOption::default();
|
||||||
let field_type = FieldType::URL;
|
let field_type = FieldType::URL;
|
||||||
let field = FieldBuilder::from_field_type(field_type).build();
|
let field = FieldBuilder::from_field_type(field_type).build();
|
||||||
assert_url(
|
assert_url(
|
||||||
&type_option,
|
&type_option,
|
||||||
"AppFlowy website - https://www.appflowy.io",
|
"https://www.appflowy.io",
|
||||||
"AppFlowy website - https://www.appflowy.io",
|
"https://www.appflowy.io",
|
||||||
"https://www.appflowy.io/",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"AppFlowy website appflowy.io",
|
|
||||||
"AppFlowy website appflowy.io",
|
|
||||||
"https://appflowy.io",
|
|
||||||
&field,
|
&field,
|
||||||
);
|
);
|
||||||
|
assert_url(&type_option, "123", "123", &field);
|
||||||
|
assert_url(&type_option, "", "", &field);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// if there's a http url and some words following it in the input string.
|
fn assert_url(type_option: &URLTypeOption, input_str: &str, expected_url: &str, _field: &Field) {
|
||||||
#[test]
|
|
||||||
fn url_type_option_contains_url_with_string_after_test() {
|
|
||||||
let type_option = URLTypeOption::default();
|
|
||||||
let field_type = FieldType::URL;
|
|
||||||
let field = FieldBuilder::from_field_type(field_type).build();
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"AppFlowy website - https://www.appflowy.io welcome!",
|
|
||||||
"AppFlowy website - https://www.appflowy.io welcome!",
|
|
||||||
"https://www.appflowy.io/",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"AppFlowy website appflowy.io welcome!",
|
|
||||||
"AppFlowy website appflowy.io welcome!",
|
|
||||||
"https://appflowy.io",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// if there's a http url and special words following it in the input string.
|
|
||||||
#[test]
|
|
||||||
fn url_type_option_contains_url_with_special_string_after_test() {
|
|
||||||
let type_option = URLTypeOption::default();
|
|
||||||
let field_type = FieldType::URL;
|
|
||||||
let field = FieldBuilder::from_field_type(field_type).build();
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"AppFlowy website - https://www.appflowy.io!",
|
|
||||||
"AppFlowy website - https://www.appflowy.io!",
|
|
||||||
"https://www.appflowy.io/",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"AppFlowy website appflowy.io!",
|
|
||||||
"AppFlowy website appflowy.io!",
|
|
||||||
"https://appflowy.io",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// if there's a level4 url in the input string.
|
|
||||||
#[test]
|
|
||||||
fn level4_url_type_test() {
|
|
||||||
let type_option = URLTypeOption::default();
|
|
||||||
let field_type = FieldType::URL;
|
|
||||||
let field = FieldBuilder::from_field_type(field_type).build();
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"test - https://tester.testgroup.appflowy.io",
|
|
||||||
"test - https://tester.testgroup.appflowy.io",
|
|
||||||
"https://tester.testgroup.appflowy.io/",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"test tester.testgroup.appflowy.io",
|
|
||||||
"test tester.testgroup.appflowy.io",
|
|
||||||
"https://tester.testgroup.appflowy.io",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// urls with different top level domains.
|
|
||||||
#[test]
|
|
||||||
fn different_top_level_domains_test() {
|
|
||||||
let type_option = URLTypeOption::default();
|
|
||||||
let field_type = FieldType::URL;
|
|
||||||
let field = FieldBuilder::from_field_type(field_type).build();
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"appflowy - https://appflowy.com",
|
|
||||||
"appflowy - https://appflowy.com",
|
|
||||||
"https://appflowy.com/",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"appflowy - https://appflowy.top",
|
|
||||||
"appflowy - https://appflowy.top",
|
|
||||||
"https://appflowy.top/",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"appflowy - https://appflowy.net",
|
|
||||||
"appflowy - https://appflowy.net",
|
|
||||||
"https://appflowy.net/",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_url(
|
|
||||||
&type_option,
|
|
||||||
"appflowy - https://appflowy.edu",
|
|
||||||
"appflowy - https://appflowy.edu",
|
|
||||||
"https://appflowy.edu/",
|
|
||||||
&field,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_url(
|
|
||||||
type_option: &URLTypeOption,
|
|
||||||
input_str: &str,
|
|
||||||
expected_str: &str,
|
|
||||||
expected_url: &str,
|
|
||||||
_field: &Field,
|
|
||||||
) {
|
|
||||||
let decode_cell_data = type_option
|
let decode_cell_data = type_option
|
||||||
.apply_changeset(input_str.to_owned(), None)
|
.apply_changeset(input_str.to_owned(), None)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.1;
|
.1;
|
||||||
assert_eq!(expected_str.to_owned(), decode_cell_data.data);
|
assert_eq!(expected_url.to_owned(), decode_cell_data.data);
|
||||||
assert_eq!(expected_url.to_owned(), decode_cell_data.url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use collab::core::any_map::AnyMapExtension;
|
||||||
|
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
|
||||||
|
use collab_database::rows::Cell;
|
||||||
|
use flowy_error::FlowyResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::entities::{TextFilterPB, URLCellDataPB};
|
use crate::entities::{TextFilterPB, URLCellDataPB};
|
||||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
|
@ -6,15 +14,6 @@ use crate::services::field::{
|
||||||
};
|
};
|
||||||
use crate::services::sort::SortCondition;
|
use crate::services::sort::SortCondition;
|
||||||
|
|
||||||
use collab::core::any_map::AnyMapExtension;
|
|
||||||
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
|
|
||||||
use collab_database::rows::Cell;
|
|
||||||
use fancy_regex::Regex;
|
|
||||||
use flowy_error::FlowyResult;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct URLTypeOption {
|
pub struct URLTypeOption {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
@ -82,14 +81,7 @@ impl CellDataChangeset for URLTypeOption {
|
||||||
changeset: <Self as TypeOption>::CellChangeset,
|
changeset: <Self as TypeOption>::CellChangeset,
|
||||||
_cell: Option<Cell>,
|
_cell: Option<Cell>,
|
||||||
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
||||||
let mut url = "".to_string();
|
let url_cell_data = URLCellData { data: changeset };
|
||||||
if let Ok(Some(m)) = URL_REGEX.find(&changeset) {
|
|
||||||
url = auto_append_scheme(m.as_str());
|
|
||||||
}
|
|
||||||
let url_cell_data = URLCellData {
|
|
||||||
url,
|
|
||||||
data: changeset,
|
|
||||||
};
|
|
||||||
Ok((url_cell_data.clone().into(), url_cell_data))
|
Ok((url_cell_data.clone().into(), url_cell_data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,26 +116,3 @@ impl TypeOptionCellDataCompare for URLTypeOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auto_append_scheme(s: &str) -> String {
|
|
||||||
// Only support https scheme by now
|
|
||||||
match url::Url::parse(s) {
|
|
||||||
Ok(url) => {
|
|
||||||
if url.scheme() == "https" {
|
|
||||||
url.into()
|
|
||||||
} else {
|
|
||||||
format!("https://{}", s)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
format!("https://{}", s)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref URL_REGEX: Regex = Regex::new(
|
|
||||||
"[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)"
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,14 +11,12 @@ use crate::services::field::{TypeOptionCellData, CELL_DATA};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct URLCellData {
|
pub struct URLCellData {
|
||||||
pub url: String,
|
|
||||||
pub data: String,
|
pub data: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl URLCellData {
|
impl URLCellData {
|
||||||
pub fn new(s: &str) -> Self {
|
pub fn new(s: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
url: "".to_string(),
|
|
||||||
data: s.to_string(),
|
data: s.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,16 +34,14 @@ impl TypeOptionCellData for URLCellData {
|
||||||
|
|
||||||
impl From<&Cell> for URLCellData {
|
impl From<&Cell> for URLCellData {
|
||||||
fn from(cell: &Cell) -> Self {
|
fn from(cell: &Cell) -> Self {
|
||||||
let url = cell.get_str_value("url").unwrap_or_default();
|
let data = cell.get_str_value(CELL_DATA).unwrap_or_default();
|
||||||
let content = cell.get_str_value(CELL_DATA).unwrap_or_default();
|
Self { data }
|
||||||
Self { url, data: content }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<URLCellData> for Cell {
|
impl From<URLCellData> for Cell {
|
||||||
fn from(data: URLCellData) -> Self {
|
fn from(data: URLCellData) -> Self {
|
||||||
new_cell_builder(FieldType::URL)
|
new_cell_builder(FieldType::URL)
|
||||||
.insert_str_value("url", data.url)
|
|
||||||
.insert_str_value(CELL_DATA, data.data)
|
.insert_str_value(CELL_DATA, data.data)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -53,19 +49,13 @@ impl From<URLCellData> for Cell {
|
||||||
|
|
||||||
impl From<URLCellData> for URLCellDataPB {
|
impl From<URLCellData> for URLCellDataPB {
|
||||||
fn from(data: URLCellData) -> Self {
|
fn from(data: URLCellData) -> Self {
|
||||||
Self {
|
Self { content: data.data }
|
||||||
url: data.url,
|
|
||||||
content: data.data,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<URLCellDataPB> for URLCellData {
|
impl From<URLCellDataPB> for URLCellData {
|
||||||
fn from(data: URLCellDataPB) -> Self {
|
fn from(data: URLCellDataPB) -> Self {
|
||||||
Self {
|
Self { data: data.content }
|
||||||
url: data.url,
|
|
||||||
data: data.content,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ impl GroupCustomize for URLGroupController {
|
||||||
) -> FlowyResult<(Option<InsertedGroupPB>, Option<GroupPB>)> {
|
) -> FlowyResult<(Option<InsertedGroupPB>, Option<GroupPB>)> {
|
||||||
// Just return if the group with this url already exists
|
// Just return if the group with this url already exists
|
||||||
let mut inserted_group = None;
|
let mut inserted_group = None;
|
||||||
if self.context.get_group(&_cell_data.url).is_none() {
|
if self.context.get_group(&_cell_data.content).is_none() {
|
||||||
let cell_data: URLCellData = _cell_data.clone().into();
|
let cell_data: URLCellData = _cell_data.clone().into();
|
||||||
let group = Group::new(cell_data.data);
|
let group = Group::new(cell_data.data);
|
||||||
let mut new_group = self.context.add_new_group(group)?;
|
let mut new_group = self.context.add_new_group(group)?;
|
||||||
|
|
|
@ -110,7 +110,10 @@ async fn url_cell_data_test() {
|
||||||
if let Some(cell) = row_cell.cell.as_ref() {
|
if let Some(cell) = row_cell.cell.as_ref() {
|
||||||
let cell = URLCellData::from(cell);
|
let cell = URLCellData::from(cell);
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
assert_eq!(cell.url.as_str(), "https://www.appflowy.io/");
|
assert_eq!(
|
||||||
|
cell.data.as_str(),
|
||||||
|
"AppFlowy website - https://www.appflowy.io"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue