Skip to content

Commit

Permalink
feat(database): basic table view grouping support (#4727)
Browse files Browse the repository at this point in the history
  • Loading branch information
zzj3720 committed Sep 21, 2023
1 parent 8291918 commit c956087
Show file tree
Hide file tree
Showing 41 changed files with 1,583 additions and 918 deletions.
1 change: 1 addition & 0 deletions packages/blocks/src/__internal__/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type MultiSelection = {
export type TableViewSelection = {
viewId: string;
type: 'table';
groupKey?: string;
rowsSelection?: MultiSelection;
columnsSelection?: MultiSelection;
focus: CellFocus;
Expand Down
6 changes: 4 additions & 2 deletions packages/blocks/src/components/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type GroupMenu = MenuCommon & {
name: string;
children: () => NormalMenu[];
};
// eslint-disable-next-line @typescript-eslint/ban-types
type MenuClass = (string & {}) | 'delete-item';
type NormalMenu = MenuCommon &
(
| {
Expand All @@ -41,7 +43,7 @@ type NormalMenu = MenuCommon &
postfix?: TemplateResult;
select: () => void;
onHover?: (hover: boolean) => void;
class?: string;
class?: MenuClass;
}
| {
type: 'checkbox';
Expand All @@ -60,7 +62,7 @@ type NormalMenu = MenuCommon &
options: MenuOptions;
}
);
type Menu = GroupMenu | NormalMenu;
export type Menu = GroupMenu | NormalMenu;
type GetMenuByType<T extends Menu['type'], M extends Menu = Menu> = M extends {
type: T;
}
Expand Down
34 changes: 24 additions & 10 deletions packages/blocks/src/components/tags/multi-tag-select.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ShadowlessElement, WithDisposable } from '@blocksuite/lit';
import { nanoid } from '@blocksuite/store';
import { autoPlacement, offset } from '@floating-ui/dom';
import type { Middleware } from '@floating-ui/dom';
import { autoPlacement, detectOverflow } from '@floating-ui/dom';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
Expand Down Expand Up @@ -485,15 +486,28 @@ export const popTagSelect = (
};
const remove = createPopup(target, component, {
onClose: ops.onComplete,
middleware: [
offset(state => {
return {
mainAxis: -state.rects.reference.height,
crossAxis:
state.rects.floating.width / 2 - state.rects.reference.width / 2,
};
}),
],
middleware: [middleware],
});
return remove;
};
const middleware: Middleware = {
name: 'middleware',
fn: async state => {
const overflow = await detectOverflow(state);
const referenceRect = state.elements.reference.getBoundingClientRect();
const top = referenceRect.top;
const left = referenceRect.left;
let y = top - 12;
let x = left - 12;
if (overflow.bottom > 0) {
y = top - state.elements.floating.getBoundingClientRect().height;
}
if (overflow.right > 0) {
x = left - overflow.right;
}
return {
y,
x,
};
},
};
1 change: 1 addition & 0 deletions packages/blocks/src/components/tags/multi-tag-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class MultiTagView extends WithDisposable(ShadowlessElement) {
align-items: center;
width: 100%;
height: 100%;
min-height: 22px;
}
.affine-select-cell-container * {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,17 @@ export class MultiSelectCellEditing extends BaseCellRenderer<

private popTagSelect = () => {
this._disposables.add({
dispose: popTagSelect(this, {
options: this._options,
onOptionsChange: this._onOptionsChange,
value: this._value,
onChange: this._onChange,
onComplete: this._editComplete,
minWidth: 400,
}),
dispose: popTagSelect(
this.querySelector('affine-multi-tag-view') ?? this,
{
options: this._options,
onOptionsChange: this._onOptionsChange,
value: this._value,
onChange: this._onChange,
onComplete: this._editComplete,
minWidth: 400,
}
),
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,18 @@ export class SelectCellEditing extends BaseCellRenderer<

private popTagSelect = () => {
this._disposables.add({
dispose: popTagSelect(this, {
mode: 'single',
options: this._options,
onOptionsChange: this._onOptionsChange,
value: this._value,
onChange: this._onChange,
onComplete: this._editComplete,
minWidth: 400,
}),
dispose: popTagSelect(
this.querySelector('affine-multi-tag-view') ?? this,
{
mode: 'single',
options: this._options,
onOptionsChange: this._onOptionsChange,
value: this._value,
onChange: this._onChange,
onComplete: this._editComplete,
minWidth: 400,
}
),
});
};

Expand Down
208 changes: 208 additions & 0 deletions packages/blocks/src/database-block/common/group-by/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import type { TType } from '../../logical/typesystem.js';
import type { InsertPosition } from '../../types.js';
import { insertPositionToIndex } from '../../utils/insert.js';
import type { DataViewManager } from '../data-view-manager.js';
import type { GroupBy, GroupProperty } from '../types.js';
import type { GroupByConfig } from './matcher.js';
import { groupByMatcher } from './matcher.js';

export type GroupData = {
key: string;
name: string;
helper: GroupHelper;
type: TType;
value: unknown;
rows: string[];
};

export class GroupHelper {
constructor(
private groupBy: GroupBy,
config: GroupByConfig,
type: TType,
private viewManager: DataViewManager,
private ops: {
sortGroup: (keys: string[]) => string[];
sortRow: (groupKey: string, rowIds: string[]) => string[];
changeGroupSort: (keys: string[]) => void;
changeRowSort: (
groupKeys: string[],
groupKey: string,
keys: string[]
) => void;
}
) {
this.groupMap = Object.fromEntries(
config.defaultKeys(type).map(({ key, value }) => [
key,
{
key,
name: config.groupName(type, value),
helper: this,
type,
value,
rows: [],
},
])
);
this.viewManager.rows.forEach(id => {
const value = this.viewManager.cellGetJsonValue(id, groupBy.columnId);
const keys = config.valuesGroup(value, type);
keys.forEach(({ key, value }) => {
if (!this.groupMap[key]) {
this.groupMap[key] = {
key,
name: config.groupName(type, value),
helper: this,
value,
rows: [],
type,
};
}
this.groupMap[key].rows.push(id);
});
});
const sortedGroup = ops.sortGroup(Object.keys(this.groupMap));
sortedGroup.forEach(key => {
this.groupMap[key].rows = ops.sortRow(key, this.groupMap[key].rows);
});
this.groups = sortedGroup.map(key => this.groupMap[key]);
}

get dataType() {
return this.viewManager.columnGetDataType(this.groupBy.columnId);
}

get column() {
return this.viewManager.columnGet(this.groupBy.columnId);
}

get columnId() {
return this.groupBy.columnId;
}

get type() {
return this.viewManager.columnGetType(this.columnId);
}

get data() {
return this.viewManager.columnGetData(this.columnId);
}

updateData = (data: NonNullable<unknown>) => {
this.viewManager.columnUpdateData(this.columnId, data);
};

updateValue(rows: string[], value: unknown) {
rows.forEach(id => {
this.viewManager.cellUpdateValue(id, this.columnId, value);
});
}

public readonly groups: GroupData[];
public readonly groupMap: Record<string, GroupData>;

groupConfig() {
return groupByMatcher.findData(v => v.name === this.groupBy.name);
}

defaultGroupProperty(key: string): GroupProperty {
return {
key,
hide: false,
manuallyCardSort: [],
};
}

addToGroup(rowId: string, key: string) {
const columnId = this.columnId;
const addTo = this.groupConfig()?.addToGroup ?? (value => value);
const newValue = addTo(
this.groupMap[key].value,
this.viewManager.cellGetJsonValue(rowId, columnId)
);
this.viewManager.cellUpdateValue(rowId, columnId, newValue);
}

removeFromGroup(rowId: string, key: string) {
const columnId = this.columnId;
const remove = this.groupConfig()?.removeFromGroup ?? (() => undefined);
const newValue = remove(
this.groupMap[key].value,
this.viewManager.cellGetJsonValue(rowId, columnId)
);
this.viewManager.cellUpdateValue(rowId, columnId, newValue);
}

changeCardSort(groupKey: string, cardIds: string[]) {
this.ops.changeRowSort(
this.groups.map(v => v.key),
groupKey,
cardIds
);
}

changeGroupSort(keys: string[]) {
this.ops.changeGroupSort(keys);
}

public moveGroupTo(groupKey: string, position: InsertPosition) {
const keys = this.groups.map(v => v.key);
keys.splice(
keys.findIndex(key => key === groupKey),
1
);
const index = insertPositionToIndex(position, keys, key => key);
keys.splice(index, 0, groupKey);
this.changeGroupSort(keys);
}

moveCardTo(
rowId: string,
fromGroupKey: string | undefined,
toGroupKey: string,
position: InsertPosition
) {
if (fromGroupKey !== toGroupKey) {
const columnId = this.columnId;
const remove = this.groupConfig()?.removeFromGroup ?? (() => undefined);
const group =
fromGroupKey != null ? this.groupMap[fromGroupKey] : undefined;
let newValue: unknown = undefined;
if (group) {
newValue = remove(
group.value,
this.viewManager.cellGetJsonValue(rowId, columnId)
);
}
const addTo = this.groupConfig()?.addToGroup ?? (value => value);
newValue = addTo(this.groupMap[toGroupKey].value, newValue);
this.viewManager.cellUpdateValue(rowId, columnId, newValue);
}
const rows = this.groupMap[toGroupKey].rows.filter(id => id !== rowId);
const index = insertPositionToIndex(position, rows, id => id);
rows.splice(index, 0, rowId);
this.changeCardSort(toGroupKey, rows);
}
get addGroup() {
return this.viewManager.columnConfigManager.getColumn(this.column.type).ops
.addGroup;
}
}
export const sortByManually = <T>(
arr: T[],
getId: (v: T) => string,
ids: string[]
) => {
const map = new Map(arr.map(v => [getId(v), v]));
const result: T[] = [];
for (const id of ids) {
const value = map.get(id);
if (value) {
map.delete(id);
result.push(value);
}
}
result.push(...map.values());
return result;
};
Loading

2 comments on commit c956087

@vercel
Copy link

@vercel vercel bot commented on c956087 Sep 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

blocksuite – ./packages/playground

blocksuite-toeverything.vercel.app
blocksuite-git-master-toeverything.vercel.app
blocksuite-five.vercel.app

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Size Report

Bundles

Entry Size Gzip Brotli
examples/basic 10.8 MB (+74.7 kB) 2.13 MB (+12.8 kB) 1.33 MB (+8.44 kB)

Packages

Name Size Gzip Brotli
blocks 1.47 MB (+10.2 kB) 368 kB (+2.04 kB) 279 kB (+1.65 kB)
editor 8.82 kB 3.33 kB 2.91 kB
store 59.6 kB 17.2 kB 15.4 kB
virgo 29.6 kB 8.48 kB 7.62 kB

Please sign in to comment.