mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-04 19:11:57 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
|
||||
import { createContextKey } from '../../core/index.js';
|
||||
|
||||
export const ShowQuickSettingBarContextKey = createContextKey<
|
||||
Signal<Record<string, boolean>>
|
||||
>('show-quick-setting-bar', signal({}));
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
import {
|
||||
menu,
|
||||
popFilterableSimpleMenu,
|
||||
popMenu,
|
||||
type PopupTarget,
|
||||
popupTargetFromElement,
|
||||
subMenuMiddleware,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
ArrowRightSmallIcon,
|
||||
DeleteIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { getRefType } from '../../../core/expression/ref/ref.js';
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import { filterMatcher } from '../../../core/filter/filter-fn/matcher.js';
|
||||
import { literalItemsMatcher } from '../../../core/filter/literal/index.js';
|
||||
import type { Filter, SingleFilter } from '../../../core/filter/types.js';
|
||||
import {
|
||||
renderUniLit,
|
||||
t,
|
||||
type TypeInstance,
|
||||
typeSystem,
|
||||
} from '../../../core/index.js';
|
||||
|
||||
export class FilterConditionView extends SignalWatcher(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
filter-condition-view {
|
||||
}
|
||||
|
||||
.filter-condition-expression {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.filter-condition-delete {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: max-content;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-condition-delete:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.filter-condition-delete svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.filter-condition-function-name {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--affine-text-secondary-color);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-condition-function-name:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.filter-condition-arg {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
padding: 0 4px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
private onClickButton = (evt: Event) => {
|
||||
this.popConditionEdit(
|
||||
popupTargetFromElement(evt.currentTarget as HTMLElement)
|
||||
);
|
||||
};
|
||||
|
||||
private popConditionEdit = (target: PopupTarget) => {
|
||||
const type = this.leftVar$.value?.type;
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
const fn = this.fnConfig$.value;
|
||||
if (!fn) {
|
||||
popFilterableSimpleMenu(target, this.getFunctionItems(target));
|
||||
return;
|
||||
}
|
||||
const handler = popMenu(target, {
|
||||
options: {
|
||||
items: [
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name: fn.label,
|
||||
postfix: ArrowRightSmallIcon(),
|
||||
select: ele => {
|
||||
popMenu(popupTargetFromElement(ele), {
|
||||
options: {
|
||||
items: [
|
||||
menu.group({
|
||||
items: this.getFunctionItems(target, () => {
|
||||
handler.close();
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
middleware: subMenuMiddleware,
|
||||
});
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
menu.dynamic(() => this.getArgsItems()),
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Delete',
|
||||
class: { 'delete-item': true },
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
const list = this.value.value.slice();
|
||||
list.splice(this.index, 1);
|
||||
this.onChange(list);
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value!: ReadonlySignal<Filter[]>;
|
||||
|
||||
filter$ = computed(() => {
|
||||
const filter = this.value.value[this.index];
|
||||
if (!filter || filter.type !== 'filter') {
|
||||
return;
|
||||
}
|
||||
return filter;
|
||||
});
|
||||
|
||||
args$ = computed(() => {
|
||||
return this.filter$.value?.args.map(v => v.value);
|
||||
});
|
||||
|
||||
fnConfig$ = computed(() => {
|
||||
return filterMatcher.getFilterByName(this.filter$.value?.function);
|
||||
});
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor vars!: ReadonlySignal<Variable[]>;
|
||||
|
||||
fnType$ = computed(() => {
|
||||
const fnConfig = this.fnConfig$.value;
|
||||
const filter = this.filter$.value;
|
||||
if (!fnConfig || !filter) {
|
||||
return;
|
||||
}
|
||||
const refType = getRefType(this.vars.value, filter.left);
|
||||
if (!refType) {
|
||||
return;
|
||||
}
|
||||
const fnTemplate = t.fn.instance(
|
||||
[fnConfig.self, ...fnConfig.args],
|
||||
t.boolean.instance(),
|
||||
fnConfig.vars
|
||||
);
|
||||
return typeSystem.instanceFn(
|
||||
fnTemplate,
|
||||
[refType],
|
||||
t.boolean.instance(),
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
getFunctionItems = (target: PopupTarget, onSelect?: () => void) => {
|
||||
const filter = this.filter$.value;
|
||||
if (!filter) {
|
||||
return [];
|
||||
}
|
||||
const type = getRefType(this.vars.value, filter?.left);
|
||||
if (!type) {
|
||||
return [];
|
||||
}
|
||||
return filterMatcher.filterListBySelfType(type).map(v => {
|
||||
const selected = v.name === filter.function;
|
||||
return menu.action({
|
||||
name: v.label,
|
||||
isSelected: selected,
|
||||
select: () => {
|
||||
this.setFilter({
|
||||
...filter,
|
||||
function: v.name,
|
||||
});
|
||||
onSelect?.();
|
||||
this.popConditionEdit(target);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
leftVar$ = computed(() => {
|
||||
return this.vars.value.find(v => v.id === this.filter$.value?.left.name);
|
||||
});
|
||||
|
||||
setFilter = (filter: SingleFilter) => {
|
||||
const list = this.value.value.slice();
|
||||
list[this.index] = filter;
|
||||
this.onChange(list);
|
||||
};
|
||||
|
||||
text$ = computed(() => {
|
||||
const name = this.leftVar$.value?.name ?? '';
|
||||
const data = this.fnConfig$.value;
|
||||
const type = this.fnType$.value;
|
||||
const argValues = this.args$.value;
|
||||
if (!type || !argValues || !data) {
|
||||
return;
|
||||
}
|
||||
const argDataList = argValues.map((v, i) =>
|
||||
v ? { value: v, type: type.args[i + 1] } : undefined
|
||||
);
|
||||
const valueString = data.shortString?.(...argDataList) ?? '';
|
||||
if (valueString) {
|
||||
return `${name}${valueString}`;
|
||||
}
|
||||
return name;
|
||||
});
|
||||
|
||||
private getArgItems(argType: TypeInstance, index: number) {
|
||||
return literalItemsMatcher.getItems(
|
||||
argType,
|
||||
computed(() => {
|
||||
return this.filter$.value?.args[index]?.value;
|
||||
}),
|
||||
value => {
|
||||
const filter = this.filter$.value;
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
const args = filter.args.slice();
|
||||
args[index] = { type: 'literal', value };
|
||||
this.setFilter({
|
||||
...filter,
|
||||
args: args,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getArgsItems() {
|
||||
return (
|
||||
this.fnType$.value?.args
|
||||
.slice(1)
|
||||
.flatMap((arg, i) => this.getArgItems(arg, i)) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const leftVar = this.leftVar$.value;
|
||||
if (!leftVar) {
|
||||
return html` <data-view-component-button
|
||||
hoverType="border"
|
||||
.text="${html`Invalid filter rule`}"
|
||||
></data-view-component-button>`;
|
||||
}
|
||||
return html`
|
||||
<data-view-component-button
|
||||
hoverType="border"
|
||||
.icon="${renderUniLit(leftVar.icon)}"
|
||||
@click="${this.onClickButton}"
|
||||
.text="${html`<span
|
||||
style="overflow: hidden;max-width: 230px;text-overflow: ellipsis"
|
||||
>${this.text$.value}</span
|
||||
>`}"
|
||||
.postfix="${ArrowDownSmallIcon()}"
|
||||
></data-view-component-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor index!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onChange!: (filters: Filter[]) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'filter-condition-view': FilterConditionView;
|
||||
}
|
||||
}
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
import {
|
||||
menu,
|
||||
popFilterableSimpleMenu,
|
||||
popMenu,
|
||||
type PopupTarget,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
ConvertIcon,
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { firstFilter, firstFilterInGroup } from '../../../core/filter/utils.js';
|
||||
|
||||
export const popAddNewFilter = (
|
||||
target: PopupTarget,
|
||||
props: {
|
||||
value: FilterGroup;
|
||||
onChange: (value: FilterGroup) => void;
|
||||
vars: Variable[];
|
||||
}
|
||||
) => {
|
||||
popFilterableSimpleMenu(target, [
|
||||
menu.action({
|
||||
name: 'Add filter',
|
||||
select: () => {
|
||||
props.onChange({
|
||||
...props.value,
|
||||
conditions: [...props.value.conditions, firstFilter(props.vars)],
|
||||
});
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Add filter group',
|
||||
select: () => {
|
||||
props.onChange({
|
||||
...props.value,
|
||||
conditions: [
|
||||
...props.value.conditions,
|
||||
firstFilterInGroup(props.vars),
|
||||
],
|
||||
});
|
||||
},
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
export class FilterGroupView extends SignalWatcher(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
filter-group-view {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.filter-group-op {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
padding: 4px;
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.filter-group-op-clickable {
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-group-op-clickable:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.filter-group-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.filter-group-button {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
|
||||
.filter-group-button svg {
|
||||
fill: var(--affine-text-secondary-color);
|
||||
color: var(--affine-text-secondary-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.filter-group-button:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.filter-group-button:hover svg {
|
||||
fill: var(--affine-text-primary-color);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.filter-group-item {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-group-item-ops {
|
||||
margin-top: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
height: max-content;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-group-item-ops:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.filter-group-item-ops svg {
|
||||
fill: var(--affine-text-secondary-color);
|
||||
color: var(--affine-text-secondary-color);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.filter-group-item-ops:hover svg {
|
||||
fill: var(--affine-text-primary-color);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.delete-style {
|
||||
background-color: var(--affine-background-error-color);
|
||||
}
|
||||
|
||||
.filter-group-border {
|
||||
border: 1px dashed var(--affine-border-color);
|
||||
}
|
||||
|
||||
.filter-group-bg-1 {
|
||||
background-color: var(--affine-background-secondary-color);
|
||||
border: 1px solid var(--affine-border-color);
|
||||
}
|
||||
|
||||
.filter-group-bg-2 {
|
||||
background-color: var(--affine-background-tertiary-color);
|
||||
border: 1px solid var(--affine-border-color);
|
||||
}
|
||||
|
||||
.hover-style {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.delete-style {
|
||||
background-color: var(--affine-background-error-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private _addNew = (e: MouseEvent) => {
|
||||
if (this.isMaxDepth) {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: [
|
||||
...this.filterGroup.value.conditions,
|
||||
firstFilter(this.vars.value),
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
popAddNewFilter(popupTargetFromElement(e.currentTarget as HTMLElement), {
|
||||
value: this.filterGroup.value,
|
||||
onChange: this.onChange,
|
||||
vars: this.vars.value,
|
||||
});
|
||||
};
|
||||
|
||||
private _selectOp = (event: MouseEvent) => {
|
||||
popFilterableSimpleMenu(
|
||||
popupTargetFromElement(event.currentTarget as HTMLElement),
|
||||
[
|
||||
menu.action({
|
||||
name: 'And',
|
||||
select: () => {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
op: 'and',
|
||||
});
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Or',
|
||||
select: () => {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
op: 'or',
|
||||
});
|
||||
},
|
||||
}),
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
private _setFilter = (index: number, filter: Filter) => {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: this.filterGroup.value.conditions.map((v, i) =>
|
||||
index === i ? filter : v
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
private opMap = {
|
||||
and: 'And',
|
||||
or: 'Or',
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor filterGroup!: ReadonlySignal<FilterGroup>;
|
||||
|
||||
conditions$ = computed(() => {
|
||||
return this.filterGroup.value.conditions;
|
||||
});
|
||||
|
||||
setConditions = (conditions: Filter[]) => {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: conditions,
|
||||
});
|
||||
};
|
||||
|
||||
private get isMaxDepth() {
|
||||
return this.depth === 3;
|
||||
}
|
||||
|
||||
private _clickConditionOps(target: HTMLElement, i: number) {
|
||||
const filter = this.filterGroup.value.conditions[i];
|
||||
popFilterableSimpleMenu(popupTargetFromElement(target), [
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name:
|
||||
filter.type === 'filter' ? 'Turn into group' : 'Wrap in group',
|
||||
prefix: ConvertIcon(),
|
||||
onHover: hover => {
|
||||
this.containerClass = hover
|
||||
? { index: i, class: 'hover-style' }
|
||||
: undefined;
|
||||
},
|
||||
hide: () => this.depth + getDepth(filter) > 3,
|
||||
select: () => {
|
||||
this.onChange({
|
||||
type: 'group',
|
||||
op: 'and',
|
||||
conditions: [this.filterGroup.value],
|
||||
});
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Duplicate',
|
||||
prefix: DuplicateIcon(),
|
||||
onHover: hover => {
|
||||
this.containerClass = hover
|
||||
? { index: i, class: 'hover-style' }
|
||||
: undefined;
|
||||
},
|
||||
select: () => {
|
||||
const conditions = [...this.filterGroup.value.conditions];
|
||||
conditions.splice(
|
||||
i + 1,
|
||||
0,
|
||||
JSON.parse(JSON.stringify(conditions[i]))
|
||||
);
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: conditions,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
menu.group({
|
||||
name: '',
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Delete',
|
||||
prefix: DeleteIcon(),
|
||||
class: { 'delete-item': true },
|
||||
onHover: hover => {
|
||||
this.containerClass = hover
|
||||
? { index: i, class: 'delete-style' }
|
||||
: undefined;
|
||||
},
|
||||
select: () => {
|
||||
const conditions = [...this.filterGroup.value.conditions];
|
||||
conditions.splice(i, 1);
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const data = this.filterGroup.value;
|
||||
return html`
|
||||
<div class="filter-group-container">
|
||||
${repeat(data.conditions, (filter, i) => {
|
||||
const clickOps = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._clickConditionOps(e.target as HTMLElement, i);
|
||||
};
|
||||
let op: TemplateResult;
|
||||
if (i === 0) {
|
||||
op = html` <div class="filter-group-op">Where</div>`;
|
||||
} else {
|
||||
op = html`
|
||||
<div
|
||||
class="filter-group-op filter-group-op-clickable"
|
||||
@click="${this._selectOp}"
|
||||
>
|
||||
${this.opMap[data.op]}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const classList = classMap({
|
||||
'filter-root-item': true,
|
||||
'filter-exactly-hover-container': true,
|
||||
'dv-pd-4 dv-round-4': true,
|
||||
[this.containerClass?.class ?? '']:
|
||||
this.containerClass?.index === i,
|
||||
});
|
||||
const groupClassList = classMap({
|
||||
[`filter-group-bg-${this.depth}`]: filter.type !== 'filter',
|
||||
});
|
||||
return html` <div class="${classList}" @contextmenu="${clickOps}">
|
||||
${op}
|
||||
<div
|
||||
style="flex:1;display:flex;align-items:start;justify-content: space-between;gap: 8px;"
|
||||
>
|
||||
${filter.type === 'filter'
|
||||
? html`
|
||||
<filter-condition-view
|
||||
.vars="${this.vars}"
|
||||
.index="${i}"
|
||||
.value="${this.conditions$}"
|
||||
.onChange="${this.setConditions}"
|
||||
></filter-condition-view>
|
||||
`
|
||||
: html`
|
||||
<filter-group-view
|
||||
class="${groupClassList}"
|
||||
style="width: 100%;"
|
||||
.depth="${this.depth + 1}"
|
||||
.onChange="${(v: Filter) => this._setFilter(i, v)}"
|
||||
.vars="${this.vars}"
|
||||
.filterGroup="${computed(() => filter)}"
|
||||
></filter-group-view>
|
||||
`}
|
||||
<div class="filter-group-item-ops" @click="${clickOps}">
|
||||
${MoreHorizontalIcon()}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
})}
|
||||
</div>
|
||||
<div class="filter-group-button" @click="${this._addNew}">
|
||||
${PlusIcon()} Add ${this.isMaxDepth ? nothing : ArrowDownSmallIcon()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor containerClass:
|
||||
| {
|
||||
index: number;
|
||||
class: string;
|
||||
}
|
||||
| undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor depth = 1;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onChange!: (filter: FilterGroup) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor vars!: ReadonlySignal<Variable[]>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'filter-group-view': FilterGroupView;
|
||||
}
|
||||
}
|
||||
export const getDepth = (filter: Filter): number => {
|
||||
if (filter.type === 'filter') {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(...filter.conditions.map(getDepth)) + 1;
|
||||
};
|
||||
export const popFilterGroup = (
|
||||
target: PopupTarget,
|
||||
props: {
|
||||
vars: ReadonlySignal<Variable[]>;
|
||||
value$: ReadonlySignal<FilterGroup>;
|
||||
onChange: (value?: FilterGroup) => void;
|
||||
onBack?: () => void;
|
||||
}
|
||||
) => {
|
||||
popMenu(target, {
|
||||
options: {
|
||||
title: {
|
||||
text: 'Filter group',
|
||||
onBack: props.onBack,
|
||||
},
|
||||
items: [
|
||||
menu.group({
|
||||
items: [
|
||||
() => {
|
||||
return html` <filter-group-view
|
||||
.vars="${props.vars}"
|
||||
.filterGroup="${props.value$}"
|
||||
.onChange="${props.onChange}"
|
||||
></filter-group-view>`;
|
||||
},
|
||||
],
|
||||
}),
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Delete',
|
||||
class: { 'delete-item': true },
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
props.onChange();
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { filterTraitKey } from '../../../core/filter/trait.js';
|
||||
import type { DataViewWidgetProps } from '../../../core/widget/types.js';
|
||||
|
||||
export const renderFilterBar = (props: DataViewWidgetProps) => {
|
||||
const filterTrait = props.dataViewInstance.view.traitGet(filterTraitKey);
|
||||
if (!filterTrait) {
|
||||
return;
|
||||
}
|
||||
if (!IS_MOBILE && !filterTrait.hasFilter$.value) {
|
||||
return;
|
||||
}
|
||||
return html` <filter-bar
|
||||
.vars="${filterTrait.view.vars$}"
|
||||
.filterGroup="${filterTrait.filter$}"
|
||||
.onChange="${filterTrait.filterSet}"
|
||||
></filter-bar>`;
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
type PopupTarget,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
FilterIcon,
|
||||
PlusIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import { popFilterGroup } from './group-panel-view.js';
|
||||
|
||||
export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
filter-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: scroll;
|
||||
margin-bottom: -10px;
|
||||
padding-bottom: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-group-tag {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
background-color: var(--affine-white);
|
||||
}
|
||||
|
||||
.filter-bar-add-filter {
|
||||
white-space: nowrap;
|
||||
color: var(--affine-text-secondary-color);
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
filter-bar::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
filter-bar::-webkit-scrollbar-thumb {
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
filter-bar::-webkit-scrollbar:horizontal {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
filter-bar:hover::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: var(--affine-black-30);
|
||||
}
|
||||
|
||||
filter-bar:hover::-webkit-scrollbar-track {
|
||||
//background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private _setFilter = (index: number, filter: Filter) => {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: this.filterGroup.value.conditions.map((v, i) =>
|
||||
index === i ? filter : v
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
private addFilter = (e: MouseEvent) => {
|
||||
const element = popupTargetFromElement(e.target as HTMLElement);
|
||||
popCreateFilter(element, {
|
||||
vars: this.vars,
|
||||
onSelect: filter => {
|
||||
const index = this.filterGroup.value.conditions.length;
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: [...this.filterGroup.value.conditions, filter],
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
this.expandGroup(element, index);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private expandGroup = (position: PopupTarget, i: number) => {
|
||||
if (this.filterGroup.value.conditions[i]?.type !== 'group') {
|
||||
return;
|
||||
}
|
||||
popFilterGroup(position, {
|
||||
vars: this.vars,
|
||||
value$: computed(() => {
|
||||
return this.filterGroup.value.conditions[i] as FilterGroup;
|
||||
}),
|
||||
onChange: filter => {
|
||||
if (filter) {
|
||||
this._setFilter(i, filter);
|
||||
} else {
|
||||
this.deleteFilter(i);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor filterGroup!: ReadonlySignal<FilterGroup>;
|
||||
|
||||
conditions$ = computed(() => {
|
||||
return this.filterGroup.value.conditions;
|
||||
});
|
||||
|
||||
renderAddFilter = () => {
|
||||
return html` <div
|
||||
style="height: 100%;"
|
||||
class="filter-bar-add-filter dv-icon-16 dv-round-4 dv-hover"
|
||||
@click="${this.addFilter}"
|
||||
>
|
||||
${PlusIcon()} Add filter
|
||||
</div>`;
|
||||
};
|
||||
|
||||
setConditions = (conditions: Filter[]) => {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: conditions,
|
||||
});
|
||||
};
|
||||
|
||||
updateMoreFilterPanel?: () => void;
|
||||
|
||||
private deleteFilter(i: number) {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: this.filterGroup.value.conditions.filter(
|
||||
(_, index) => index !== i
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html` ${this.renderFilters()} ${this.renderAddFilter()} `;
|
||||
}
|
||||
|
||||
renderCondition(i: number) {
|
||||
const condition = this.conditions$.value[i];
|
||||
if (!condition) {
|
||||
return;
|
||||
}
|
||||
if (condition.type === 'filter') {
|
||||
return html` <filter-condition-view
|
||||
.vars="${this.vars}"
|
||||
.index="${i}"
|
||||
.value="${this.conditions$}"
|
||||
.onChange="${this.setConditions}"
|
||||
></filter-condition-view>`;
|
||||
}
|
||||
const expandGroup = (e: MouseEvent) => {
|
||||
this.expandGroup(
|
||||
popupTargetFromElement(e.currentTarget as HTMLElement),
|
||||
i
|
||||
);
|
||||
};
|
||||
const length = condition.conditions.length;
|
||||
const text = length > 1 ? `${length} rules` : `${length} rule`;
|
||||
return html` <data-view-component-button
|
||||
hoverType="border"
|
||||
.icon="${FilterIcon()}"
|
||||
@click="${expandGroup}"
|
||||
.text="${html`${text}`}"
|
||||
.postfix="${ArrowDownSmallIcon()}"
|
||||
></data-view-component-button>`;
|
||||
}
|
||||
|
||||
renderFilters() {
|
||||
return this.filterGroup.value.conditions.map((_, i) =>
|
||||
this.renderCondition(i)
|
||||
);
|
||||
}
|
||||
|
||||
override updated() {
|
||||
this.updateMoreFilterPanel?.();
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onChange!: (filter: FilterGroup) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor vars!: ReadonlySignal<Variable[]>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'filter-bar': FilterBar;
|
||||
}
|
||||
}
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
import {
|
||||
menu,
|
||||
popFilterableSimpleMenu,
|
||||
popMenu,
|
||||
type PopupTarget,
|
||||
popupTargetFromElement,
|
||||
subMenuMiddleware,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
ConvertIcon,
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
FilterIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { FilterTrait } from '../../../core/filter/trait.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import {
|
||||
type FilterGroupView,
|
||||
getDepth,
|
||||
popFilterGroup,
|
||||
} from './group-panel-view.js';
|
||||
|
||||
export class FilterRootView extends SignalWatcher(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
.filter-root-title {
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.filter-root-op {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
padding: 4px;
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-root-op-clickable {
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-root-op-clickable:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.filter-root-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.filter-root-button {
|
||||
margin: 4px 8px 8px;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
|
||||
.filter-root-button svg {
|
||||
fill: var(--affine-text-secondary-color);
|
||||
color: var(--affine-text-secondary-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.filter-root-button:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.filter-root-button:hover svg {
|
||||
fill: var(--affine-text-primary-color);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.filter-root-item {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-group-title {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--affine-text-primary-color);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-root-item-ops {
|
||||
margin-top: 2px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
height: max-content;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-root-item-ops:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.filter-root-item-ops svg {
|
||||
fill: var(--affine-text-secondary-color);
|
||||
color: var(--affine-text-secondary-color);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.filter-root-item-ops:hover svg {
|
||||
fill: var(--affine-text-primary-color);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.filter-root-grabber {
|
||||
cursor: grab;
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
background-color: var(--affine-placeholder-color);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--affine-divider-color);
|
||||
flex-shrink: 0;
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
private _setFilter = (index: number, filter: Filter) => {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: this.filterGroup.value.conditions.map((v, i) =>
|
||||
index === i ? filter : v
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
private expandGroup = (position: PopupTarget, i: number) => {
|
||||
if (this.filterGroup.value.conditions[i]?.type !== 'group') {
|
||||
return;
|
||||
}
|
||||
popFilterGroup(position, {
|
||||
vars: this.vars,
|
||||
value$: computed(() => {
|
||||
return this.filterGroup.value.conditions[i] as FilterGroup;
|
||||
}),
|
||||
onChange: filter => {
|
||||
if (filter) {
|
||||
this._setFilter(i, filter);
|
||||
} else {
|
||||
this.deleteFilter(i);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor filterGroup!: ReadonlySignal<FilterGroup>;
|
||||
|
||||
conditions$ = computed(() => {
|
||||
return this.filterGroup.value.conditions;
|
||||
});
|
||||
|
||||
setConditions = (conditions: Filter[]) => {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: conditions,
|
||||
});
|
||||
};
|
||||
|
||||
private _clickConditionOps(target: HTMLElement, i: number) {
|
||||
const filter = this.filterGroup.value.conditions[i];
|
||||
popFilterableSimpleMenu(popupTargetFromElement(target), [
|
||||
menu.action({
|
||||
name: filter.type === 'filter' ? 'Turn into group' : 'Wrap in group',
|
||||
prefix: ConvertIcon(),
|
||||
onHover: hover => {
|
||||
this.containerClass = hover
|
||||
? { index: i, class: 'hover-style' }
|
||||
: undefined;
|
||||
},
|
||||
hide: () => getDepth(filter) > 3,
|
||||
select: () => {
|
||||
this.onChange({
|
||||
type: 'group',
|
||||
op: 'and',
|
||||
conditions: [this.filterGroup.value],
|
||||
});
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Duplicate',
|
||||
prefix: DuplicateIcon(),
|
||||
onHover: hover => {
|
||||
this.containerClass = hover
|
||||
? { index: i, class: 'hover-style' }
|
||||
: undefined;
|
||||
},
|
||||
select: () => {
|
||||
const conditions = [...this.filterGroup.value.conditions];
|
||||
conditions.splice(
|
||||
i + 1,
|
||||
0,
|
||||
JSON.parse(JSON.stringify(conditions[i]))
|
||||
);
|
||||
this.onChange({ ...this.filterGroup.value, conditions: conditions });
|
||||
},
|
||||
}),
|
||||
menu.group({
|
||||
name: '',
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Delete',
|
||||
prefix: DeleteIcon(),
|
||||
class: { 'delete-item': true },
|
||||
onHover: hover => {
|
||||
this.containerClass = hover
|
||||
? { index: i, class: 'delete-style' }
|
||||
: undefined;
|
||||
},
|
||||
select: () => {
|
||||
const conditions = [...this.filterGroup.value.conditions];
|
||||
conditions.splice(i, 1);
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
private deleteFilter(i: number) {
|
||||
this.onChange({
|
||||
...this.filterGroup.value,
|
||||
conditions: this.filterGroup.value.conditions.filter(
|
||||
(_, index) => index !== i
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const data = this.filterGroup.value;
|
||||
return html`
|
||||
<div class="filter-root-container">
|
||||
${repeat(data.conditions, (_, i) => {
|
||||
const clickOps = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._clickConditionOps(e.target as HTMLElement, i);
|
||||
};
|
||||
const ops = html`
|
||||
<div class="filter-root-item-ops" @click="${clickOps}">
|
||||
${MoreHorizontalIcon()}
|
||||
</div>
|
||||
`;
|
||||
const content = html`
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content: space-between;width: 100%;gap:8px;"
|
||||
>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<div class="filter-root-grabber"></div>
|
||||
${this.renderCondition(i)}
|
||||
</div>
|
||||
${ops}
|
||||
</div>
|
||||
`;
|
||||
const classList = classMap({
|
||||
'filter-root-item': true,
|
||||
'filter-exactly-hover-container': true,
|
||||
'dv-pd-4 dv-round-4': true,
|
||||
[this.containerClass?.class ?? '']:
|
||||
this.containerClass?.index === i,
|
||||
});
|
||||
return html` <div @contextmenu="${clickOps}" class="${classList}">
|
||||
${content}
|
||||
</div>`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderCondition(i: number) {
|
||||
const condition = this.conditions$.value[i];
|
||||
if (!condition) {
|
||||
return;
|
||||
}
|
||||
if (condition.type === 'filter') {
|
||||
return html` <filter-condition-view
|
||||
.vars="${this.vars}"
|
||||
.index="${i}"
|
||||
.value="${this.conditions$}"
|
||||
.onChange="${this.setConditions}"
|
||||
></filter-condition-view>`;
|
||||
}
|
||||
const expandGroup = (e: MouseEvent) => {
|
||||
this.expandGroup(
|
||||
popupTargetFromElement(e.currentTarget as HTMLElement),
|
||||
i
|
||||
);
|
||||
};
|
||||
const length = condition.conditions.length;
|
||||
const text = length > 1 ? `${length} rules` : `${length} rule`;
|
||||
return html` <data-view-component-button
|
||||
hoverType="border"
|
||||
.icon="${FilterIcon()}"
|
||||
@click="${expandGroup}"
|
||||
.text="${html`${text}`}"
|
||||
.postfix="${ArrowDownSmallIcon()}"
|
||||
></data-view-component-button>`;
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor containerClass:
|
||||
| {
|
||||
index: number;
|
||||
class: string;
|
||||
}
|
||||
| undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onBack!: () => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onChange!: (filter: FilterGroup) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor vars!: ReadonlySignal<Variable[]>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'filter-root-view': FilterGroupView;
|
||||
}
|
||||
}
|
||||
export const popFilterRoot = (
|
||||
target: PopupTarget,
|
||||
props: {
|
||||
filterTrait: FilterTrait;
|
||||
onBack: () => void;
|
||||
}
|
||||
) => {
|
||||
const filterTrait = props.filterTrait;
|
||||
const view = filterTrait.view;
|
||||
popMenu(target, {
|
||||
options: {
|
||||
title: {
|
||||
text: 'Filters',
|
||||
onBack: props.onBack,
|
||||
},
|
||||
items: [
|
||||
menu.group({
|
||||
items: [
|
||||
() => {
|
||||
return html` <filter-root-view
|
||||
.onBack="${props.onBack}"
|
||||
.vars="${view.vars$}"
|
||||
.filterGroup="${filterTrait.filter$}"
|
||||
.onChange="${filterTrait.filterSet}"
|
||||
></filter-root-view>`;
|
||||
},
|
||||
],
|
||||
}),
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Add',
|
||||
prefix: PlusIcon(),
|
||||
select: ele => {
|
||||
const value = filterTrait.filter$.value;
|
||||
popCreateFilter(
|
||||
popupTargetFromElement(ele),
|
||||
{
|
||||
vars: view.vars$,
|
||||
onSelect: filter => {
|
||||
filterTrait.filterSet({
|
||||
...value,
|
||||
conditions: [...value.conditions, filter],
|
||||
});
|
||||
},
|
||||
},
|
||||
{ middleware: subMenuMiddleware }
|
||||
);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||
import { html, nothing } from 'lit';
|
||||
|
||||
import {
|
||||
type DataViewWidgetProps,
|
||||
defineUniComponent,
|
||||
} from '../../core/index.js';
|
||||
import { ShowQuickSettingBarContextKey } from './context.js';
|
||||
import { renderFilterBar } from './filter/index.js';
|
||||
import { renderSortBar } from './sort/index.js';
|
||||
|
||||
export const widgetQuickSettingBar = defineUniComponent(
|
||||
(props: DataViewWidgetProps) => {
|
||||
const view = props.dataViewInstance.view;
|
||||
const barList = [renderSortBar(props), renderFilterBar(props)].filter(
|
||||
Boolean
|
||||
);
|
||||
if (!IS_MOBILE) {
|
||||
if (!view.contextGet(ShowQuickSettingBarContextKey).value[view.id]) {
|
||||
return html``;
|
||||
}
|
||||
if (!barList.length) {
|
||||
return html``;
|
||||
}
|
||||
}
|
||||
return html` <div
|
||||
style="display: flex;margin-top: 8px;align-items: center;width: 100%;gap:8px"
|
||||
>
|
||||
${barList.map((bar, index) => {
|
||||
return html`
|
||||
${index !== 0
|
||||
? html` <div
|
||||
style="width: 1px;height:27px;background-color: ${unsafeCSSVarV2(
|
||||
'layer/insideBorder/border'
|
||||
)}"
|
||||
></div>`
|
||||
: nothing}
|
||||
${bar}
|
||||
`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
|
||||
import { SortIcon } from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { sortTraitKey } from '../../../core/sort/manager.js';
|
||||
import { createSortUtils } from '../../../core/sort/utils.js';
|
||||
import type { DataViewWidgetProps } from '../../../core/widget/types.js';
|
||||
import { popSortRoot } from './root-panel.js';
|
||||
|
||||
export const renderSortBar = (props: DataViewWidgetProps) => {
|
||||
const sortTrait = props.dataViewInstance.view.traitGet(sortTraitKey);
|
||||
if (!sortTrait) {
|
||||
return;
|
||||
}
|
||||
const count = sortTrait.sortList$.value.length;
|
||||
if (count === 0) {
|
||||
return;
|
||||
}
|
||||
const text = count === 1 ? html`1 Sort` : html`${count} Sorts`;
|
||||
const click = (event: MouseEvent) => {
|
||||
popSortRoot(popupTargetFromElement(event.currentTarget as HTMLElement), {
|
||||
sortUtils: createSortUtils(sortTrait, props.dataViewInstance.eventTrace),
|
||||
});
|
||||
};
|
||||
return html` <data-view-component-button
|
||||
class="data-view-sort-button"
|
||||
.onClick="${click}"
|
||||
hoverType="border"
|
||||
.icon="${SortIcon()}"
|
||||
.text="${text}"
|
||||
></data-view-component-button>`;
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
type PopupTarget,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
CloseIcon,
|
||||
DeleteIcon,
|
||||
PlusIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { renderUniLit } from '../../../core/index.js';
|
||||
import { popCreateSort } from '../../../core/sort/add-sort.js';
|
||||
import type { SortBy } from '../../../core/sort/types.js';
|
||||
import type { SortUtils } from '../../../core/sort/utils.js';
|
||||
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
|
||||
import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
|
||||
import {
|
||||
createSortContext,
|
||||
sortable,
|
||||
} from '../../../core/utils/wc-dnd/sort/sort-context.js';
|
||||
import { verticalListSortingStrategy } from '../../../core/utils/wc-dnd/sort/strategies/index.js';
|
||||
|
||||
export class SortRootView extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.sort-root-container {
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sort-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor sortUtils!: SortUtils;
|
||||
|
||||
items$ = computed(() => {
|
||||
return this.sortUtils.sortList$.value.map(v => v.ref.name);
|
||||
});
|
||||
|
||||
sortContext = createSortContext({
|
||||
activators: defaultActivators,
|
||||
container: this,
|
||||
onDragEnd: evt => {
|
||||
const over = evt.over;
|
||||
if (over) {
|
||||
const list = this.sortUtils.sortList$.value;
|
||||
this.sortUtils.move(
|
||||
list.findIndex(v => v.ref.name === evt.active.id),
|
||||
list.findIndex(v => v.ref.name === over.id)
|
||||
);
|
||||
}
|
||||
},
|
||||
modifiers: [
|
||||
({ transform }) => {
|
||||
return {
|
||||
...transform,
|
||||
x: 0,
|
||||
};
|
||||
},
|
||||
],
|
||||
items: this.items$,
|
||||
strategy: verticalListSortingStrategy,
|
||||
});
|
||||
|
||||
override render() {
|
||||
const list = this.sortUtils.sortList$.value;
|
||||
return html`
|
||||
<div class="sort-root-container">
|
||||
${repeat(list, (sort, index) => {
|
||||
const id = sort.ref.name;
|
||||
const variable = this.sortUtils.vars$.value.find(v => v.id === id);
|
||||
let content;
|
||||
const deleteRule = () => {
|
||||
this.sortUtils.remove(index);
|
||||
};
|
||||
const changeRule = (rule: SortBy) => {
|
||||
this.sortUtils.change(index, rule);
|
||||
};
|
||||
if (!variable) {
|
||||
content = html`
|
||||
<data-view-component-button
|
||||
style="color: var(--affine-error-color);border-color: color: var(--affine-error-color)"
|
||||
@click="${deleteRule}"
|
||||
.text="${html`This rule is invalid, click to delete`}"
|
||||
></data-view-component-button>
|
||||
`;
|
||||
} else {
|
||||
const descName = sort.desc ? 'Descending' : 'Ascending';
|
||||
const clickField = (event: MouseEvent) => {
|
||||
popMenu(
|
||||
popupTargetFromElement(event.currentTarget as HTMLElement),
|
||||
{
|
||||
options: {
|
||||
items: this.sortUtils.vars$.value.map(v => {
|
||||
return menu.action({
|
||||
name: v.name,
|
||||
prefix: renderUniLit(v.icon),
|
||||
isSelected: v.id === id,
|
||||
select: () => {
|
||||
changeRule({
|
||||
...sort,
|
||||
ref: { type: 'ref', name: v.id },
|
||||
});
|
||||
},
|
||||
});
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
const clickOrder = (event: MouseEvent) => {
|
||||
popMenu(
|
||||
popupTargetFromElement(event.currentTarget as HTMLElement),
|
||||
{
|
||||
options: {
|
||||
items: [false, true].map(desc => {
|
||||
return menu.action({
|
||||
name: desc ? 'Descending' : 'Ascending',
|
||||
isSelected: desc === sort.desc,
|
||||
select: () => {
|
||||
changeRule({ ...sort, desc });
|
||||
},
|
||||
});
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
content = html`
|
||||
<data-view-component-button
|
||||
style="margin-right: 6px;margin-left: 4px"
|
||||
@click="${clickField}"
|
||||
.icon="${renderUniLit(variable.icon)}"
|
||||
.text="${variable.name}"
|
||||
.postfix="${ArrowDownSmallIcon()}"
|
||||
></data-view-component-button>
|
||||
<data-view-component-button
|
||||
@click="${clickOrder}"
|
||||
.text="${html` <div style="padding: 0 4px">${descName}</div>`}"
|
||||
.postfix="${ArrowDownSmallIcon()}"
|
||||
></data-view-component-button>
|
||||
`;
|
||||
}
|
||||
return keyed(
|
||||
id,
|
||||
html`
|
||||
<div
|
||||
${sortable(id)}
|
||||
class='sort-item'
|
||||
>
|
||||
<div style='display: flex;align-items: center;flex:1;margin-right: 16px;'>
|
||||
<div
|
||||
${dragHandler(id)}
|
||||
style='border-radius:2px;cursor:pointer;width: 4px;height: 12px;background-color: ${unsafeCSSVarV2(
|
||||
'button/grabber/default'
|
||||
)}'
|
||||
></div>
|
||||
${content}
|
||||
</div>
|
||||
<div
|
||||
@click='${deleteRule}'
|
||||
style='padding: 2px;display: flex;align-items: center;border-radius: 2px;color:${unsafeCSSVarV2('icon/primary')}'
|
||||
class='dv-hover dv-rounded'>${CloseIcon({ width: '16px', height: '16px' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sort-root-view': SortRootView;
|
||||
}
|
||||
}
|
||||
|
||||
export const popSortRoot = (
|
||||
target: PopupTarget,
|
||||
props: {
|
||||
sortUtils: SortUtils;
|
||||
title?: {
|
||||
text: string;
|
||||
onBack?: () => void;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
const sortUtils = props.sortUtils;
|
||||
popMenu(target, {
|
||||
options: {
|
||||
title: props.title,
|
||||
items: [
|
||||
() => {
|
||||
return html` <sort-root-view
|
||||
.sortUtils="${sortUtils}"
|
||||
></sort-root-view>`;
|
||||
},
|
||||
menu.action({
|
||||
name: 'Add sort',
|
||||
prefix: PlusIcon(),
|
||||
select: ele => {
|
||||
popCreateSort(popupTargetFromElement(ele), {
|
||||
sortUtils: props.sortUtils,
|
||||
});
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete',
|
||||
class: { 'delete-item': true },
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
props.sortUtils.removeAll();
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user