feat(filter): support memo location filters (#6181)
This commit is contained in:
parent
da81b6b48f
commit
57bfe0a70f
57 changed files with 264 additions and 14 deletions
|
|
@ -16,9 +16,8 @@ extending or debugging the package.
|
||||||
`parser.go` or extend the renderer helpers.
|
`parser.go` or extend the renderer helpers.
|
||||||
- Keep validation in the parser (e.g., reject unsupported operators).
|
- Keep validation in the parser (e.g., reject unsupported operators).
|
||||||
3. **Write a golden test**
|
3. **Write a golden test**
|
||||||
- Extend the dialect-specific memo filter tests under
|
- Extend `engine_test.go` with per-dialect rendering assertions for the new
|
||||||
`store/db/{sqlite,mysql,postgres}/memo_filter_test.go` with a case that
|
field, and a SQLite behavioral test when the semantics warrant it.
|
||||||
exercises the new field.
|
|
||||||
4. **Run `go test ./...`** to ensure the SQL output matches expectations across
|
4. **Run `go test ./...`** to ensure the SQL output matches expectations across
|
||||||
all dialects.
|
all dialects.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,11 @@ stmt, _ := engine.CompileToStatement(ctx, `has_task_list && visibility == "PUBLI
|
||||||
present in the memo tag set, not through prefix matching.
|
present in the memo tag set, not through prefix matching.
|
||||||
- **Boolean Flags** — Fields such as `has_task_list` render as `IS TRUE` equality
|
- **Boolean Flags** — Fields such as `has_task_list` render as `IS TRUE` equality
|
||||||
checks, or comparisons against `CAST('true' AS JSON)` depending on the dialect.
|
checks, or comparisons against `CAST('true' AS JSON)` depending on the dialect.
|
||||||
|
- **Presence Flags** — `has_location` renders as a JSON key-existence check on
|
||||||
|
`memo.payload` (`$.location`). A missing key and an explicit JSON null both
|
||||||
|
count as absent on every dialect; any other value — including an empty object —
|
||||||
|
counts as present. Only `==`/`!=` against a boolean literal (or bare/negated
|
||||||
|
use) is allowed.
|
||||||
- **String Matching** — `content.contains(x)`, `content.startsWith(x)`, and
|
- **String Matching** — `content.contains(x)`, `content.startsWith(x)`, and
|
||||||
`content.endsWith(x)` render as case-insensitive `LIKE`/`ILIKE` with LIKE
|
`content.endsWith(x)` render as case-insensitive `LIKE`/`ILIKE` with LIKE
|
||||||
metacharacters (`%`, `_`, `\`) escaped. Available on scalar string fields whose
|
metacharacters (`%`, `_`, `\`) escaped. Available on scalar string fields whose
|
||||||
|
|
|
||||||
|
|
@ -513,3 +513,106 @@ func TestTagComprehensionAvoidsMySQLExistsSemiJoin(t *testing.T) {
|
||||||
require.Contains(t, stmt.SQL, "EXISTS (SELECT 1", "dialect %s", dialect)
|
require.Contains(t, stmt.SQL, "EXISTS (SELECT 1", "dialect %s", dialect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderHasLocationPerDialect(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
engine, err := NewEngine(NewSchema())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
dialect DialectName
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{DialectSQLite, "JSON_EXTRACT(`memo`.`payload`, '$.location') IS NOT NULL"},
|
||||||
|
{DialectMySQL, "COALESCE(JSON_TYPE(JSON_EXTRACT(`memo`.`payload`, '$.location')), 'NULL') != 'NULL'"},
|
||||||
|
{DialectPostgres, "memo.payload->>'location' IS NOT NULL"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
stmt, err := engine.CompileToStatement(context.Background(), `has_location`, RenderOptions{Dialect: tc.dialect})
|
||||||
|
require.NoError(t, err, tc.dialect)
|
||||||
|
require.Equal(t, tc.sql, stmt.SQL, tc.dialect)
|
||||||
|
require.Empty(t, stmt.Args, tc.dialect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderHasLocationNegationAndComparisons(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
engine, err := NewEngine(NewSchema())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
const exists = "JSON_EXTRACT(`memo`.`payload`, '$.location') IS NOT NULL"
|
||||||
|
cases := []struct {
|
||||||
|
expr string
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{`has_location`, exists},
|
||||||
|
{`!has_location`, "NOT (" + exists + ")"},
|
||||||
|
{`has_location == true`, exists},
|
||||||
|
{`has_location == false`, "NOT (" + exists + ")"},
|
||||||
|
{`has_location != true`, "NOT (" + exists + ")"},
|
||||||
|
{`has_location != false`, exists},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
stmt, err := engine.CompileToStatement(context.Background(), tc.expr, RenderOptions{Dialect: DialectSQLite})
|
||||||
|
require.NoError(t, err, tc.expr)
|
||||||
|
require.Equal(t, tc.sql, stmt.SQL, tc.expr)
|
||||||
|
require.Empty(t, stmt.Args, tc.expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompileRejectsOrderingOnHasLocation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
engine, err := NewEngine(NewSchema())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Only ==/!= are meaningful for a presence flag.
|
||||||
|
_, err = engine.Compile(context.Background(), `has_location < true`)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHasLocationSQLiteBehavior pins the presence semantics against a real
|
||||||
|
// database: a missing key, an explicit JSON null, and a NULL payload all count
|
||||||
|
// as absent, while any location object — even an empty one — counts as present.
|
||||||
|
func TestHasLocationSQLiteBehavior(t *testing.T) {
|
||||||
|
db, err := sql.Open("sqlite", ":memory:")
|
||||||
|
require.NoError(t, err)
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
t.Cleanup(func() { require.NoError(t, db.Close()) })
|
||||||
|
|
||||||
|
_, err = db.Exec(`CREATE TABLE memo (id INTEGER PRIMARY KEY, payload TEXT)`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
for _, fixture := range []struct {
|
||||||
|
id int
|
||||||
|
payload any
|
||||||
|
}{
|
||||||
|
{1, `{}`},
|
||||||
|
{2, `{"location":{"placeholder":"Tokyo","latitude":35.6,"longitude":139.7}}`},
|
||||||
|
{3, `{"location":{}}`},
|
||||||
|
{4, `{"location":null}`},
|
||||||
|
{5, nil},
|
||||||
|
} {
|
||||||
|
_, err = db.Exec(`INSERT INTO memo (id, payload) VALUES (?, ?)`, fixture.id, fixture.payload)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := NewEngine(NewSchema())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
expr string
|
||||||
|
want []int
|
||||||
|
}{
|
||||||
|
{`has_location`, []int{2, 3}},
|
||||||
|
{`!has_location`, []int{1, 4, 5}},
|
||||||
|
{`has_location == false`, []int{1, 4, 5}},
|
||||||
|
{`has_location != false`, []int{2, 3}},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
stmt, err := engine.CompileToStatement(context.Background(), tc.expr, RenderOptions{Dialect: DialectSQLite})
|
||||||
|
require.NoError(t, err, tc.expr)
|
||||||
|
require.Equal(t, tc.want, selectMemoIDs(t, db, stmt), tc.expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,12 @@ func (r *renderer) renderFieldPredicate(cond *FieldPredicateCondition) (renderRe
|
||||||
return renderResult{}, err
|
return renderResult{}, err
|
||||||
}
|
}
|
||||||
return renderResult{sql: sql}, nil
|
return renderResult{sql: sql}, nil
|
||||||
|
case FieldKindJSONExists:
|
||||||
|
sql, err := r.jsonExistsSQL(field)
|
||||||
|
if err != nil {
|
||||||
|
return renderResult{}, errors.Wrap(err, "failed to render JSON existence predicate")
|
||||||
|
}
|
||||||
|
return renderResult{sql: sql}, nil
|
||||||
default:
|
default:
|
||||||
return renderResult{}, errors.Errorf("field %q cannot be used as a predicate", cond.Field)
|
return renderResult{}, errors.Errorf("field %q cannot be used as a predicate", cond.Field)
|
||||||
}
|
}
|
||||||
|
|
@ -160,6 +166,8 @@ func (r *renderer) renderComparison(cond *ComparisonCondition) (renderResult, er
|
||||||
return r.renderBoolColumnComparison(field, cond.Operator, cond.Right)
|
return r.renderBoolColumnComparison(field, cond.Operator, cond.Right)
|
||||||
case FieldKindJSONBool:
|
case FieldKindJSONBool:
|
||||||
return r.renderJSONBoolComparison(field, cond.Operator, cond.Right)
|
return r.renderJSONBoolComparison(field, cond.Operator, cond.Right)
|
||||||
|
case FieldKindJSONExists:
|
||||||
|
return r.renderJSONExistsComparison(field, cond.Operator, cond.Right)
|
||||||
case FieldKindScalar:
|
case FieldKindScalar:
|
||||||
return r.renderScalarComparison(field, cond.Operator, cond.Right)
|
return r.renderScalarComparison(field, cond.Operator, cond.Right)
|
||||||
default:
|
default:
|
||||||
|
|
@ -725,6 +733,49 @@ func tagLikePattern(mode TextMatchMode, value string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsonExistsSQL renders a predicate that is true when the JSON key at the
|
||||||
|
// field's path holds a non-null value. A missing key and an explicit JSON null
|
||||||
|
// both count as absent on every dialect.
|
||||||
|
func (r *renderer) jsonExistsSQL(field Field) (string, error) {
|
||||||
|
expr := jsonExtractExpr(r.dialect, field)
|
||||||
|
switch r.dialect {
|
||||||
|
case DialectSQLite, DialectPostgres:
|
||||||
|
// SQLite's JSON_EXTRACT and Postgres' terminal ->> fold both a missing
|
||||||
|
// key and a JSON null to SQL NULL.
|
||||||
|
return fmt.Sprintf("%s IS NOT NULL", expr), nil
|
||||||
|
case DialectMySQL:
|
||||||
|
// MySQL's JSON_EXTRACT returns SQL NULL for a missing key but a JSON
|
||||||
|
// null literal for an explicit null; JSON_TYPE reports 'NULL' only for
|
||||||
|
// the latter, so both collapse to 'NULL' here.
|
||||||
|
return fmt.Sprintf("COALESCE(JSON_TYPE(%s), 'NULL') != 'NULL'", expr), nil
|
||||||
|
default:
|
||||||
|
return "", errors.Errorf("unsupported dialect %s", r.dialect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *renderer) renderJSONExistsComparison(field Field, op ComparisonOperator, right ValueExpr) (renderResult, error) {
|
||||||
|
value, err := expectBool(right)
|
||||||
|
if err != nil {
|
||||||
|
return renderResult{}, errors.Wrap(err, "json existence comparison requires a boolean value")
|
||||||
|
}
|
||||||
|
existsSQL, err := r.jsonExistsSQL(field)
|
||||||
|
if err != nil {
|
||||||
|
return renderResult{}, errors.Wrap(err, "failed to render JSON existence comparison")
|
||||||
|
}
|
||||||
|
want := value
|
||||||
|
switch op {
|
||||||
|
case CompareEq:
|
||||||
|
case CompareNeq:
|
||||||
|
want = !want
|
||||||
|
default:
|
||||||
|
return renderResult{}, errors.Errorf("operator %s not supported for field %q", op, field.Name)
|
||||||
|
}
|
||||||
|
if want {
|
||||||
|
return renderResult{sql: existsSQL}, nil
|
||||||
|
}
|
||||||
|
return renderResult{sql: fmt.Sprintf("NOT (%s)", existsSQL)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *renderer) jsonBoolPredicate(field Field) (string, error) {
|
func (r *renderer) jsonBoolPredicate(field Field) (string, error) {
|
||||||
expr := jsonExtractExpr(r.dialect, field)
|
expr := jsonExtractExpr(r.dialect, field)
|
||||||
switch r.dialect {
|
switch r.dialect {
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,11 @@ const (
|
||||||
type FieldKind string
|
type FieldKind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FieldKindScalar FieldKind = "scalar"
|
FieldKindScalar FieldKind = "scalar"
|
||||||
FieldKindBoolColumn FieldKind = "bool_column"
|
FieldKindBoolColumn FieldKind = "bool_column"
|
||||||
FieldKindJSONBool FieldKind = "json_bool"
|
FieldKindJSONBool FieldKind = "json_bool"
|
||||||
|
// FieldKindJSONExists represents a boolean derived from the presence of a non-null JSON value.
|
||||||
|
FieldKindJSONExists FieldKind = "json_exists"
|
||||||
FieldKindJSONList FieldKind = "json_list"
|
FieldKindJSONList FieldKind = "json_list"
|
||||||
FieldKindVirtualAlias FieldKind = "virtual_alias"
|
FieldKindVirtualAlias FieldKind = "virtual_alias"
|
||||||
)
|
)
|
||||||
|
|
@ -227,6 +229,17 @@ func NewSchema() Schema {
|
||||||
CompareNeq: true,
|
CompareNeq: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"has_location": {
|
||||||
|
Name: "has_location",
|
||||||
|
Kind: FieldKindJSONExists,
|
||||||
|
Type: FieldTypeBool,
|
||||||
|
Column: Column{Table: "memo", Name: "payload"},
|
||||||
|
JSONPath: []string{"location"},
|
||||||
|
AllowedComparisonOps: map[ComparisonOperator]bool{
|
||||||
|
CompareEq: true,
|
||||||
|
CompareNeq: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
envOptions := []cel.EnvOption{
|
envOptions := []cel.EnvOption{
|
||||||
|
|
@ -243,6 +256,7 @@ func NewSchema() Schema {
|
||||||
cel.Variable("has_link", cel.BoolType),
|
cel.Variable("has_link", cel.BoolType),
|
||||||
cel.Variable("has_code", cel.BoolType),
|
cel.Variable("has_code", cel.BoolType),
|
||||||
cel.Variable("has_incomplete_tasks", cel.BoolType),
|
cel.Variable("has_incomplete_tasks", cel.BoolType),
|
||||||
|
cel.Variable("has_location", cel.BoolType),
|
||||||
cel.Variable("now", cel.TimestampType),
|
cel.Variable("now", cel.TimestampType),
|
||||||
ext.Sets(),
|
ext.Sets(),
|
||||||
cel.ASTValidators(cel.ValidateRegexLiterals()),
|
cel.ASTValidators(cel.ValidateRegexLiterals()),
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,8 @@ message ListMemosRequest {
|
||||||
// created_ts / updated_ts (timestamp), pinned (bool),
|
// created_ts / updated_ts (timestamp), pinned (bool),
|
||||||
// visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
// visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
||||||
// tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
// tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
||||||
// has_task_list / has_link / has_code / has_incomplete_tasks (bool).
|
// has_task_list / has_link / has_code / has_incomplete_tasks (bool),
|
||||||
|
// has_location (bool; true when the memo has a location attached).
|
||||||
// Note: the time fields here are created_ts / updated_ts, which differ from
|
// Note: the time fields here are created_ts / updated_ts, which differ from
|
||||||
// the create_time / update_time names used by order_by.
|
// the create_time / update_time names used by order_by.
|
||||||
// Examples:
|
// Examples:
|
||||||
|
|
|
||||||
|
|
@ -550,7 +550,8 @@ type ListMemosRequest struct {
|
||||||
// created_ts / updated_ts (timestamp), pinned (bool),
|
// created_ts / updated_ts (timestamp), pinned (bool),
|
||||||
// visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
// visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
||||||
// tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
// tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
||||||
// has_task_list / has_link / has_code / has_incomplete_tasks (bool).
|
// has_task_list / has_link / has_code / has_incomplete_tasks (bool),
|
||||||
|
// has_location (bool; true when the memo has a location attached).
|
||||||
//
|
//
|
||||||
// Note: the time fields here are created_ts / updated_ts, which differ from
|
// Note: the time fields here are created_ts / updated_ts, which differ from
|
||||||
// the create_time / update_time names used by order_by.
|
// the create_time / update_time names used by order_by.
|
||||||
|
|
|
||||||
|
|
@ -664,7 +664,8 @@ paths:
|
||||||
created_ts / updated_ts (timestamp), pinned (bool),
|
created_ts / updated_ts (timestamp), pinned (bool),
|
||||||
visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
||||||
tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
||||||
has_task_list / has_link / has_code / has_incomplete_tasks (bool).
|
has_task_list / has_link / has_code / has_incomplete_tasks (bool),
|
||||||
|
has_location (bool; true when the memo has a location attached).
|
||||||
Note: the time fields here are created_ts / updated_ts, which differ from
|
Note: the time fields here are created_ts / updated_ts, which differ from
|
||||||
the create_time / update_time names used by order_by.
|
the create_time / update_time names used by order_by.
|
||||||
Examples:
|
Examples:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
HashIcon,
|
HashIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
LucideIcon,
|
LucideIcon,
|
||||||
|
MapPinIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -54,6 +55,10 @@ const FILTER_CONFIGS: Record<FilterFactor, FilterConfig> = {
|
||||||
icon: CodeIcon,
|
icon: CodeIcon,
|
||||||
getLabel: (_, t) => t("memo.filters.has-code"),
|
getLabel: (_, t) => t("memo.filters.has-code"),
|
||||||
},
|
},
|
||||||
|
"property.hasLocation": {
|
||||||
|
icon: MapPinIcon,
|
||||||
|
getLabel: (_, t) => t("memo.filters.has-location"),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MemoFilters = ({ className }: { className?: string }) => {
|
const MemoFilters = ({ className }: { className?: string }) => {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ export type FilterFactor =
|
||||||
| "pinned"
|
| "pinned"
|
||||||
| "property.hasLink"
|
| "property.hasLink"
|
||||||
| "property.hasTaskList"
|
| "property.hasTaskList"
|
||||||
| "property.hasCode";
|
| "property.hasCode"
|
||||||
|
| "property.hasLocation";
|
||||||
|
|
||||||
export interface MemoFilter {
|
export interface MemoFilter {
|
||||||
factor: FilterFactor;
|
factor: FilterFactor;
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,8 @@ export const buildMemoFilter = ({
|
||||||
conditions.push(`has_task_list`);
|
conditions.push(`has_task_list`);
|
||||||
} else if (filter.factor === "property.hasCode") {
|
} else if (filter.factor === "property.hasCode") {
|
||||||
conditions.push(`has_code`);
|
conditions.push(`has_code`);
|
||||||
|
} else if (filter.factor === "property.hasLocation") {
|
||||||
|
conditions.push(`has_location`);
|
||||||
} else if (filter.factor === "displayTime") {
|
} else if (filter.factor === "displayTime") {
|
||||||
const range = getLocalDayTimestampRange(filter.value);
|
const range = getLocalDayTimestampRange(filter.value);
|
||||||
if (range) {
|
if (range) {
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "يحتوي على كود",
|
"has-code": "يحتوي على كود",
|
||||||
"has-link": "يحتوي على رابط",
|
"has-link": "يحتوي على رابط",
|
||||||
|
"has-location": "يحتوي على موقع",
|
||||||
"has-task-list": "يحتوي على قائمة مهام",
|
"has-task-list": "يحتوي على قائمة مهام",
|
||||||
"label": "المرشحات"
|
"label": "المرشحات"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "има местоположение",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Филтри"
|
"label": "Филтри"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "téCodi",
|
"has-code": "téCodi",
|
||||||
"has-link": "téEnllaç",
|
"has-link": "téEnllaç",
|
||||||
|
"has-location": "té ubicació",
|
||||||
"has-task-list": "téLlistaTasques",
|
"has-task-list": "téLlistaTasques",
|
||||||
"label": "Filtres"
|
"label": "Filtres"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "maKod",
|
"has-code": "maKod",
|
||||||
"has-link": "maOdkaz",
|
"has-link": "maOdkaz",
|
||||||
|
"has-location": "má polohu",
|
||||||
"has-task-list": "maSeznamUkolu",
|
"has-task-list": "maSeznamUkolu",
|
||||||
"label": "Filtry"
|
"label": "Filtry"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "harLink",
|
"has-link": "harLink",
|
||||||
|
"has-location": "har placering",
|
||||||
"has-task-list": "har Opgaveliste",
|
"has-task-list": "har Opgaveliste",
|
||||||
"label": "Filtre"
|
"label": "Filtre"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hatCode",
|
"has-code": "hatCode",
|
||||||
"has-link": "hatLink",
|
"has-link": "hatLink",
|
||||||
|
"has-location": "hat Standort",
|
||||||
"has-task-list": "hatAufgabenliste",
|
"has-task-list": "hatAufgabenliste",
|
||||||
"label": "Filter"
|
"label": "Filter"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "έχει Κώδικα",
|
"has-code": "έχει Κώδικα",
|
||||||
"has-link": "έχει Σύνδεσμο",
|
"has-link": "έχει Σύνδεσμο",
|
||||||
|
"has-location": "έχει τοποθεσία",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Φίλτρα"
|
"label": "Φίλτρα"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "hasLocation",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Filters"
|
"label": "Filters"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "hasLocation",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Filters"
|
"label": "Filters"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "tieneCódigo",
|
"has-code": "tieneCódigo",
|
||||||
"has-link": "tieneEnlace",
|
"has-link": "tieneEnlace",
|
||||||
|
"has-location": "tiene ubicación",
|
||||||
"has-task-list": "tieneListaTareas",
|
"has-task-list": "tieneListaTareas",
|
||||||
"label": "Filtros"
|
"label": "Filtros"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "on asukoht",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Filtrid"
|
"label": "Filtrid"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "دارای کد",
|
"has-code": "دارای کد",
|
||||||
"has-link": "دارای پیوند",
|
"has-link": "دارای پیوند",
|
||||||
|
"has-location": "دارای موقعیت",
|
||||||
"has-task-list": "دارای لیست کارها",
|
"has-task-list": "دارای لیست کارها",
|
||||||
"label": "فیلترها"
|
"label": "فیلترها"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "on sijainti",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Suodattimet"
|
"label": "Suodattimet"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "aCode",
|
"has-code": "aCode",
|
||||||
"has-link": "aLien",
|
"has-link": "aLien",
|
||||||
|
"has-location": "a une localisation",
|
||||||
"has-task-list": "aListeTâches",
|
"has-task-list": "aListeTâches",
|
||||||
"label": "Filtres"
|
"label": "Filtres"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "ten localización",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Filtros"
|
"label": "Filtros"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "कोड है",
|
"has-code": "कोड है",
|
||||||
"has-link": "लिंक है",
|
"has-link": "लिंक है",
|
||||||
|
"has-location": "स्थान है",
|
||||||
"has-task-list": "टास्क लिस्ट है",
|
"has-task-list": "टास्क लिस्ट है",
|
||||||
"label": "फिल्टर"
|
"label": "फिल्टर"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "imaKod",
|
"has-code": "imaKod",
|
||||||
"has-link": "imaLink",
|
"has-link": "imaLink",
|
||||||
|
"has-location": "ima lokaciju",
|
||||||
"has-task-list": "imaZadatke",
|
"has-task-list": "imaZadatke",
|
||||||
"label": "Filteri"
|
"label": "Filteri"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "vanKód",
|
"has-code": "vanKód",
|
||||||
"has-link": "vanLink",
|
"has-link": "vanLink",
|
||||||
|
"has-location": "van helyadat",
|
||||||
"has-task-list": "vanFeladatLista",
|
"has-task-list": "vanFeladatLista",
|
||||||
"label": "Szűrők"
|
"label": "Szűrők"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "Memiliki kode",
|
"has-code": "Memiliki kode",
|
||||||
"has-link": "Memiliki tautan",
|
"has-link": "Memiliki tautan",
|
||||||
|
"has-location": "Memiliki lokasi",
|
||||||
"has-task-list": "Memiliki daftar tugas",
|
"has-task-list": "Memiliki daftar tugas",
|
||||||
"label": "Filter"
|
"label": "Filter"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "haCodice",
|
"has-code": "haCodice",
|
||||||
"has-link": "haLink",
|
"has-link": "haLink",
|
||||||
|
"has-location": "ha una posizione",
|
||||||
"has-task-list": "haListaCompiti",
|
"has-task-list": "haListaCompiti",
|
||||||
"label": "Filtri"
|
"label": "Filtri"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "コードあり",
|
"has-code": "コードあり",
|
||||||
"has-link": "リンクあり",
|
"has-link": "リンクあり",
|
||||||
|
"has-location": "位置情報あり",
|
||||||
"has-task-list": "タスクリストあり",
|
"has-task-list": "タスクリストあり",
|
||||||
"label": "フィルター"
|
"label": "フィルター"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "კოდიარის",
|
"has-code": "კოდიარის",
|
||||||
"has-link": "ლინკიარის",
|
"has-link": "ლინკიარის",
|
||||||
|
"has-location": "აქვს მდებარეობა",
|
||||||
"has-task-list": "სიისამოცანებია",
|
"has-task-list": "სიისამოცანებია",
|
||||||
"label": "ფილტრები"
|
"label": "ფილტრები"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "코드있음",
|
"has-code": "코드있음",
|
||||||
"has-link": "링크있음",
|
"has-link": "링크있음",
|
||||||
|
"has-location": "위치 있음",
|
||||||
"has-task-list": "할일목록있음",
|
"has-task-list": "할일목록있음",
|
||||||
"label": "필터"
|
"label": "필터"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "turi vietą",
|
||||||
"has-task-list": "turiTaskList",
|
"has-task-list": "turiTaskList",
|
||||||
"label": "Filtrai"
|
"label": "Filtrai"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "ir atrašanās vieta",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Filtri"
|
"label": "Filtri"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "कोड आहे",
|
"has-code": "कोड आहे",
|
||||||
"has-link": "लिंक आहे",
|
"has-link": "लिंक आहे",
|
||||||
|
"has-location": "स्थान आहे",
|
||||||
"has-task-list": "कार्यसूची आहे",
|
"has-task-list": "कार्यसूची आहे",
|
||||||
"label": "फिल्टर"
|
"label": "फिल्टर"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "harKode",
|
"has-code": "harKode",
|
||||||
"has-link": "harLink",
|
"has-link": "harLink",
|
||||||
|
"has-location": "har posisjon",
|
||||||
"has-task-list": "harGjøremålsListe",
|
"has-task-list": "harGjøremålsListe",
|
||||||
"label": "Filtre"
|
"label": "Filtre"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "heeftCode",
|
"has-code": "heeftCode",
|
||||||
"has-link": "heeftLink",
|
"has-link": "heeftLink",
|
||||||
|
"has-location": "heeft locatie",
|
||||||
"has-task-list": "heeftTakenlijst",
|
"has-task-list": "heeftTakenlijst",
|
||||||
"label": "Filters"
|
"label": "Filters"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "ma kod",
|
"has-code": "ma kod",
|
||||||
"has-link": "ma link",
|
"has-link": "ma link",
|
||||||
|
"has-location": "ma lokalizację",
|
||||||
"has-task-list": "ma listę zadań",
|
"has-task-list": "ma listę zadań",
|
||||||
"label": "Filtry"
|
"label": "Filtry"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "temCódigo",
|
"has-code": "temCódigo",
|
||||||
"has-link": "temLink",
|
"has-link": "temLink",
|
||||||
|
"has-location": "tem localização",
|
||||||
"has-task-list": "temListaDeTarefas",
|
"has-task-list": "temListaDeTarefas",
|
||||||
"label": "Filtros"
|
"label": "Filtros"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "temCódigo",
|
"has-code": "temCódigo",
|
||||||
"has-link": "temLink",
|
"has-link": "temLink",
|
||||||
|
"has-location": "tem localização",
|
||||||
"has-task-list": "temListaDeTarefas",
|
"has-task-list": "temListaDeTarefas",
|
||||||
"label": "Filtros"
|
"label": "Filtros"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "areCode",
|
"has-code": "areCode",
|
||||||
"has-link": "areLink",
|
"has-link": "areLink",
|
||||||
|
"has-location": "are locație",
|
||||||
"has-task-list": "areTaskList",
|
"has-task-list": "areTaskList",
|
||||||
"label": "Filtre"
|
"label": "Filtre"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "есть местоположение",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Фильтры"
|
"label": "Фильтры"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "hasCode",
|
"has-code": "hasCode",
|
||||||
"has-link": "hasLink",
|
"has-link": "hasLink",
|
||||||
|
"has-location": "má polohu",
|
||||||
"has-task-list": "hasTaskList",
|
"has-task-list": "hasTaskList",
|
||||||
"label": "Filtre"
|
"label": "Filtre"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "imaKodo",
|
"has-code": "imaKodo",
|
||||||
"has-link": "imaPovezavo",
|
"has-link": "imaPovezavo",
|
||||||
|
"has-location": "imaLokacijo",
|
||||||
"has-task-list": "imaSeznamOpravil",
|
"has-task-list": "imaSeznamOpravil",
|
||||||
"label": "Filtri"
|
"label": "Filtri"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "хасЦоде",
|
"has-code": "хасЦоде",
|
||||||
"has-link": "хасЛинк",
|
"has-link": "хасЛинк",
|
||||||
|
"has-location": "има локацију",
|
||||||
"has-task-list": "хасТаскЛист",
|
"has-task-list": "хасТаскЛист",
|
||||||
"label": "Филтери"
|
"label": "Филтери"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "harKod",
|
"has-code": "harKod",
|
||||||
"has-link": "harLänk",
|
"has-link": "harLänk",
|
||||||
|
"has-location": "har plats",
|
||||||
"has-task-list": "harAttGöraLista",
|
"has-task-list": "harAttGöraLista",
|
||||||
"label": "Filter"
|
"label": "Filter"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "มีโค้ด",
|
"has-code": "มีโค้ด",
|
||||||
"has-link": "มีลิงก์",
|
"has-link": "มีลิงก์",
|
||||||
|
"has-location": "มีตำแหน่ง",
|
||||||
"has-task-list": "มีรายการที่ต้องทำ",
|
"has-task-list": "มีรายการที่ต้องทำ",
|
||||||
"label": "ตัวกรอง"
|
"label": "ตัวกรอง"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "Kod içerir",
|
"has-code": "Kod içerir",
|
||||||
"has-link": "Bağlantı içerir",
|
"has-link": "Bağlantı içerir",
|
||||||
|
"has-location": "Konum içerir",
|
||||||
"has-task-list": "Yapılacaklar listesi içerir",
|
"has-task-list": "Yapılacaklar listesi içerir",
|
||||||
"label": "Filtreler"
|
"label": "Filtreler"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "єКод",
|
"has-code": "єКод",
|
||||||
"has-link": "єПосилання",
|
"has-link": "єПосилання",
|
||||||
|
"has-location": "є розташування",
|
||||||
"has-task-list": "єСписокЗавдань",
|
"has-task-list": "єСписокЗавдань",
|
||||||
"label": "Фільтри"
|
"label": "Фільтри"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "cóMã",
|
"has-code": "cóMã",
|
||||||
"has-link": "cóLiênKết",
|
"has-link": "cóLiênKết",
|
||||||
|
"has-location": "có vị trí",
|
||||||
"has-task-list": "cóDanhSáchViệc",
|
"has-task-list": "cóDanhSáchViệc",
|
||||||
"label": "Bộ lọc"
|
"label": "Bộ lọc"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "有代码",
|
"has-code": "有代码",
|
||||||
"has-link": "有链接",
|
"has-link": "有链接",
|
||||||
|
"has-location": "有位置",
|
||||||
"has-task-list": "有待办",
|
"has-task-list": "有待办",
|
||||||
"label": "过滤器"
|
"label": "过滤器"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,7 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"has-code": "有程式碼",
|
"has-code": "有程式碼",
|
||||||
"has-link": "有連結",
|
"has-link": "有連結",
|
||||||
|
"has-location": "有位置",
|
||||||
"has-task-list": "有待辦事項",
|
"has-task-list": "有待辦事項",
|
||||||
"label": "過濾器"
|
"label": "過濾器"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Clock3Icon,
|
Clock3Icon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FilterIcon,
|
FilterIcon,
|
||||||
|
MapPinIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
PinIcon,
|
PinIcon,
|
||||||
|
|
@ -88,6 +89,12 @@ const memoViewExamples = [
|
||||||
description: "Memos containing links or code blocks.",
|
description: "Memos containing links or code blocks.",
|
||||||
icon: FilterIcon,
|
icon: FilterIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "No location",
|
||||||
|
filter: "!has_location",
|
||||||
|
description: "Memos without an attached location.",
|
||||||
|
icon: MapPinIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Content search",
|
title: "Content search",
|
||||||
filter: 'content.contains("TODO")',
|
filter: 'content.contains("TODO")',
|
||||||
|
|
@ -170,6 +177,7 @@ const filterFields = [
|
||||||
"has_incomplete_tasks",
|
"has_incomplete_tasks",
|
||||||
"has_link",
|
"has_link",
|
||||||
"has_code",
|
"has_code",
|
||||||
|
"has_location",
|
||||||
'created_ts >= now - duration("24h")',
|
'created_ts >= now - duration("24h")',
|
||||||
"created_ts.getFullYear() == ...",
|
"created_ts.getFullYear() == ...",
|
||||||
"created_ts.getMonth() == ... (0 = Jan)",
|
"created_ts.getMonth() == ... (0 = Jan)",
|
||||||
|
|
@ -204,9 +212,10 @@ const MemoViewGuide = ({ onUseExample }: MemoViewGuideProps) => {
|
||||||
{memoViewExamples.map((example) => {
|
{memoViewExamples.map((example) => {
|
||||||
const Icon = example.icon;
|
const Icon = example.icon;
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
key={example.filter}
|
key={example.filter}
|
||||||
className="group cursor-pointer rounded-md border border-transparent p-2 text-left transition-colors hover:border-border hover:bg-muted/50"
|
className="group w-full cursor-pointer rounded-md border border-transparent p-2 text-left transition-colors hover:border-border hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||||
onClick={() => onUseExample(example)}
|
onClick={() => onUseExample(example)}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 text-sm font-medium text-foreground">
|
<span className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||||
|
|
@ -215,7 +224,7 @@ const MemoViewGuide = ({ onUseExample }: MemoViewGuideProps) => {
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 block font-mono text-xs leading-5 text-muted-foreground">{example.filter}</span>
|
<span className="mt-1 block font-mono text-xs leading-5 text-muted-foreground">{example.filter}</span>
|
||||||
<span className="mt-1 block text-xs leading-5 text-muted-foreground">{example.description}</span>
|
<span className="mt-1 block text-xs leading-5 text-muted-foreground">{example.description}</span>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,8 @@ export type ListMemosRequest = Message<"memos.api.v1.ListMemosRequest"> & {
|
||||||
* created_ts / updated_ts (timestamp), pinned (bool),
|
* created_ts / updated_ts (timestamp), pinned (bool),
|
||||||
* visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
* visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
||||||
* tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
* tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
||||||
* has_task_list / has_link / has_code / has_incomplete_tasks (bool).
|
* has_task_list / has_link / has_code / has_incomplete_tasks (bool),
|
||||||
|
* has_location (bool; true when the memo has a location attached).
|
||||||
* Note: the time fields here are created_ts / updated_ts, which differ from
|
* Note: the time fields here are created_ts / updated_ts, which differ from
|
||||||
* the create_time / update_time names used by order_by.
|
* the create_time / update_time names used by order_by.
|
||||||
* Examples:
|
* Examples:
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,20 @@ describe("memo views", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps property filter factors to their CEL flags", () => {
|
||||||
|
expect(
|
||||||
|
buildMemoFilter({
|
||||||
|
filters: [
|
||||||
|
{ factor: "property.hasLink", value: "" },
|
||||||
|
{ factor: "property.hasTaskList", value: "" },
|
||||||
|
{ factor: "property.hasCode", value: "" },
|
||||||
|
{ factor: "property.hasLocation", value: "" },
|
||||||
|
],
|
||||||
|
includePinned: false,
|
||||||
|
}),
|
||||||
|
).toBe("has_link && has_task_list && has_code && has_location");
|
||||||
|
});
|
||||||
|
|
||||||
it("uses a custom memo view filter when Tasks is not selected", () => {
|
it("uses a custom memo view filter when Tasks is not selected", () => {
|
||||||
expect(
|
expect(
|
||||||
buildMemoFilter({
|
buildMemoFilter({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue