From 098cf397b92483ec06522c7f2fa359d0e50e6663 Mon Sep 17 00:00:00 2001 From: samunohito <46447427+samunohito@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:24:47 +0900 Subject: [PATCH] add MkGrid.stories.impl.ts --- packages/frontend/.storybook/fake-utils.ts | 95 ++++++++ packages/frontend/.storybook/generate.tsx | 1 + .../src/components/grid/MkDataCell.vue | 26 ++ .../components/grid/MkGrid.stories.impl.ts | 225 ++++++++++++++++++ .../frontend/src/components/grid/MkGrid.vue | 2 +- 5 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/.storybook/fake-utils.ts create mode 100644 packages/frontend/src/components/grid/MkGrid.stories.impl.ts diff --git a/packages/frontend/.storybook/fake-utils.ts b/packages/frontend/.storybook/fake-utils.ts new file mode 100644 index 0000000000..236d646f6f --- /dev/null +++ b/packages/frontend/.storybook/fake-utils.ts @@ -0,0 +1,95 @@ +/** + * AIで生成した無作為なファーストネーム + */ +export const firstNameDict: string = [ + 'Ethan', 'Olivia', 'Jackson', 'Emma', 'Liam', 'Ava', 'Aiden', 'Sophia', 'Mason', 'Isabella', + 'Noah', 'Mia', 'Lucas', 'Harper', 'Caleb', 'Abigail', 'Samuel', 'Emily', 'Logan', + 'Madison', 'Benjamin', 'Chloe', 'Elijah', 'Grace', 'Alexander', 'Scarlett', 'William', 'Zoey', 'James', 'Lily', +] + +/** + * AIで生成した無作為なラストネーム + */ +export const lastNameDict: string = [ + 'Anderson', 'Johnson', 'Thompson', 'Davis', 'Rodriguez', 'Smith', 'Patel', 'Williams', 'Lee', 'Brown', + 'Garcia', 'Jackson', 'Martinez', 'Taylor', 'Harris', 'Nguyen', 'Miller', 'Jones', 'Wilson', + 'White', 'Thomas', 'Garcia', 'Martinez', 'Robinson', 'Turner', 'Lewis', 'Hall', 'King', 'Baker', 'Cooper', +] + +/** + * AIで生成した無作為な国名 + */ +export const countryDict: string = [ + 'Japan', 'Canada', 'Brazil', 'Australia', 'Italy', 'SouthAfrica', 'Mexico', 'Sweden', 'Russia', 'India', + 'Germany', 'Argentina', 'South Korea', 'France', 'Nigeria', 'Turkey', 'Spain', 'Egypt', 'Thailand', + 'Vietnam', 'Kenya', 'Saudi Arabia', 'Netherlands', 'Colombia', 'Poland', 'Chile', 'Malaysia', 'Ukraine', 'New Zealand', 'Peru', +] + +export function text(length: number = 10): string { + let result = ""; + + while (result.length < length) { + result += Math.random().toString(36).substring(2); + } + + return result.substring(0, length); +} + +export function integer(min: number = 0, max: number = 9999): number { + return Math.floor(Math.random() * (max - min)) + min; +} + +export function date(params?: { + yearMin?: number, + yearMax?: number, + monthMin?: number, + monthMax?: number, + dayMin?: number, + dayMax?: number, + hourMin?: number, + hourMax?: number, + minuteMin?: number, + minuteMax?: number, + secondMin?: number, + secondMax?: number, + millisecondMin?: number, + millisecondMax?: number, +}) { + const year = integer(params?.yearMin ?? 1970, params?.yearMax ?? (new Date()).getFullYear()); + const month = integer(params?.monthMin ?? 1, params?.monthMax ?? 12); + let day = integer(params?.dayMin ?? 1, params?.dayMax ?? 31); + if (month === 2) { + day = Math.min(day, 28); + } else if ([4, 6, 9, 11].includes(month)) { + day = Math.min(day, 30); + } else { + day = Math.min(day, 31); + } + + const hour = integer(params?.hourMin ?? 0, params?.hourMax ?? 23); + const minute = integer(params?.minuteMin ?? 0, params?.minuteMax ?? 59); + const second = integer(params?.secondMin ?? 0, params?.secondMax ?? 59); + const millisecond = integer(params?.millisecondMin ?? 0, params?.millisecondMax ?? 999); + + return new Date(year, month - 1, day, hour, minute, second, millisecond); +} + +export function boolean(): boolean { + return Math.random() < 0.5; +} + +export function choose(array: T[]): T { + return array[Math.floor(Math.random() * array.length)]; +} + +export function firstName(): string { + return firstNameDict[Math.floor(Math.random() * firstNameDict.length)]; +} + +export function lastName(): string { + return lastNameDict[Math.floor(Math.random() * lastNameDict.length)]; +} + +export function country(): string { + return countryDict[Math.floor(Math.random() * countryDict.length)]; +} diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 6b22022f41..66a90f6761 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -411,6 +411,7 @@ function toStories(component: string): Promise { glob('src/components/MkInviteCode.vue'), glob('src/components/MkTagItem.vue'), glob('src/components/MkRoleSelectDialog.vue'), + glob('src/components/grid/MkGrid.vue'), glob('src/pages/user/home.vue'), ]); const components = globs.flat(); diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue index d7cb8d3ae5..0e9eb5d15f 100644 --- a/packages/frontend/src/components/grid/MkDataCell.vue +++ b/packages/frontend/src/components/grid/MkDataCell.vue @@ -22,6 +22,12 @@
{{ cell.value }}
+
+ {{ cell.value }} +
+
+ {{ cell.value }} +
@@ -46,6 +52,24 @@ @mousedown.stop @contextmenu.stop /> + +
@@ -185,6 +209,8 @@ async function beginEditing(target: HTMLElement) { rootEl.value?.focus(); } else { switch (cellType.value) { + case 'number': + case 'date': case 'text': { editingValue.value = cell.value.value; editing.value = true; diff --git a/packages/frontend/src/components/grid/MkGrid.stories.impl.ts b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts new file mode 100644 index 0000000000..7ad8117722 --- /dev/null +++ b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { ref } from 'vue'; +import { commonHandlers } from '../../../.storybook/mocks.js'; +import { boolean, choose, country, date, firstName, integer, lastName, text } from '../../../.storybook/fake-utils.js'; +import MkGrid from './MkGrid.vue'; +import { GridContext, GridEvent } from '@/components/grid/grid-event.js'; +import { DataSource, GridSetting } from '@/components/grid/grid.js'; + +function d(p: { + check?: boolean, + name?: string, + email?: string, + age?: number, + birthday?: string, + gender?: string, + country?: string, + reportCount?: number, + createdAt?: string, +}) { + const prefix = text(10); + + return { + check: p.check ?? boolean(), + name: p.name ?? `${firstName()} ${lastName()}`, + email: p.email ?? `${prefix}@example.com`, + age: p.age ?? integer(20, 80), + birthday: date().toISOString(), + gender: p.gender ?? choose(['male', 'female', 'other', 'unknown']), + country: p.country ?? country(), + reportCount: p.reportCount ?? integer(), + createdAt: p.createdAt ?? date().toISOString(), + }; +} + +const defaultCols = [ + { bindTo: 'check', icon: 'ti-check', type: 'boolean', width: 50 }, + { bindTo: 'name', title: 'Name', type: 'text', width: 'auto' }, + { bindTo: 'email', title: 'Email', type: 'text', width: 'auto' }, + { bindTo: 'age', title: 'Age', type: 'number', width: 50 }, + { bindTo: 'birthday', title: 'Birthday', type: 'date', width: 'auto' }, + { bindTo: 'gender', title: 'Gender', type: 'text', width: 80 }, + { bindTo: 'country', title: 'Country', type: 'text', width: 120 }, + { bindTo: 'reportCount', title: 'ReportCount', type: 'number', width: 'auto' }, + { bindTo: 'createdAt', title: 'CreatedAt', type: 'date', width: 'auto' }, +]; + +function createArgs(overrides?: { settings?: Partial, data?: DataSource[] }) { + const refData = ref([ + d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), + d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), + d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), + d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), + d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), d({}), + ]); + + return { + settings: { + row: overrides?.settings?.row, + cols: [ + ...defaultCols.filter(col => overrides?.settings?.cols?.every(c => c.bindTo !== col.bindTo) ?? true), + ...overrides?.settings?.cols ?? [], + ], + cells: overrides?.settings?.cells, + }, + data: refData.value, + }; +} + +function createRender(params: { settings: Partial, data: DataSource[] }) { + return { + render(args) { + return { + components: { + MkGrid, + }, + setup() { + return { + args, + }; + }, + data() { + return { + data: args.data, + }; + }, + computed: { + props() { + return { + ...args, + }; + }, + events() { + return { + event: (event: GridEvent, context: GridContext) => { + switch (event.type) { + case 'cell-value-change': { + args.data[event.row.index][event.column.setting.bindTo] = event.newValue; + } + } + }, + }; + }, + }, + template: '
', + }; + }, + args: { + ...params, + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + ], + }, + }, + } satisfies StoryObj; +} + +export const Default = createRender(createArgs()); + +export const NoNumber = createRender(createArgs({ + settings: { + row: { + showNumber: false, + }, + }, +})); + +export const NoSelectable = createRender(createArgs({ + settings: { + row: { + selectable: false, + }, + }, +})); + +export const Editable = createRender(createArgs({ + settings: { + cols: defaultCols.map(col => ({ ...col, editable: true })), + }, +})); + +export const AdditionalRowStyle = createRender(createArgs({ + settings: { + cols: defaultCols.map(col => ({ ...col, editable: true })), + row: { + styleRules: [ + { + condition: ({ row }) => AdditionalRowStyle.args.data[row.index].check, + applyStyle: { + style: { + backgroundColor: 'lightgray', + }, + }, + }, + ], + }, + }, +})); + +export const ContextMenu = createRender(createArgs({ + settings: { + cols: [ + { + bindTo: 'check', icon: 'ti-check', type: 'boolean', width: 50, contextMenuFactory: (col, context) => [ + { + type: 'button', + text: 'Check All', + action: () => { + for (const d of ContextMenu.args.data) { + d.check = true; + } + }, + }, + { + type: 'button', + text: 'Uncheck All', + action: () => { + for (const d of ContextMenu.args.data) { + d.check = false; + } + }, + }, + ], + }, + ], + row: { + contextMenuFactory: (row, context) => [ + { + type: 'button', + text: 'Delete', + action: () => { + const idxes = context.rangedRows.map(r => r.index); + const newData = ContextMenu.args.data.filter((d, i) => !idxes.includes(i)); + + ContextMenu.args.data.splice(0); + ContextMenu.args.data.push(...newData); + }, + }, + ], + }, + cells: { + contextMenuFactory: (col, row, value, context) => [ + { + type: 'button', + text: 'Delete', + action: () => { + for (const cell of context.rangedCells) { + ContextMenu.args.data[cell.row.index][cell.column.setting.bindTo] = undefined; + } + }, + }, + ], + }, + }, +})); diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index 0d70b58aea..fd6339d244 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -67,7 +67,7 @@ type RowHolder = { } const emit = defineEmits<{ - (ev: 'event', event: GridEvent, current: GridContext): void; + (ev: 'event', event: GridEvent, context: GridContext): void; }>(); const props = defineProps<{