Skip to content

Commit f46b305

Browse files
authored
Merge pull request #37 from mura-/feature/arrange-hooks
Feature/arrange hooks
2 parents 3cf525f + c268ce6 commit f46b305

File tree

3 files changed

+272
-181
lines changed

3 files changed

+272
-181
lines changed

src/js/spreadsheet/hooks.ts

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { useEffect, useRef } from 'react';
2+
import Handsontable from 'handsontable';
3+
import { useAsync, useAsyncFn } from 'react-use';
4+
import { HotTable } from '@handsontable/react';
5+
import { Record } from '@kintone/rest-api-client/lib/client/types';
6+
import { client } from '~/src/js/utils/client';
7+
8+
const NOT_ALLOWED_EDIT_FIELDS = [
9+
'RECORD_NUMBER',
10+
'CREATED_TIME',
11+
'UPDATED_TIME',
12+
'CREATOR',
13+
'MODIFIER',
14+
'STATUS',
15+
'STATUS_ASSIGNEE',
16+
'CALC',
17+
'CHECK_BOX',
18+
'MULTI_SELECT',
19+
'FILE',
20+
'CATEGORY',
21+
'SUBTABLE',
22+
'ORGANIZATION_SELECT',
23+
'GROUP_SELECT',
24+
];
25+
26+
const shapingRecord = (record: Record) =>
27+
Object.fromEntries(
28+
Object.entries(record).map(([k, v]) => [
29+
k,
30+
{ ...v, value: v.type === 'NUMBER' ? v.value.replace(/[^0-9]/g, '') : v.value },
31+
]),
32+
);
33+
34+
const excludeNonEditableFields = (record: Record) =>
35+
Object.fromEntries(Object.entries(record).filter(([, v]) => NOT_ALLOWED_EDIT_FIELDS.indexOf(v.type) === -1));
36+
37+
export const useRecursiveTimeout = <T extends unknown>(
38+
callback: () => Promise<T> | (() => void),
39+
delay: number | null,
40+
): void => {
41+
const savedCallback = useRef(callback);
42+
43+
// Remember the latest callback.
44+
useEffect(() => {
45+
savedCallback.current = callback;
46+
}, [callback]);
47+
48+
// Set up the timeout loop.
49+
useEffect(() => {
50+
let id: NodeJS.Timeout;
51+
function tick() {
52+
const ret = savedCallback.current();
53+
54+
if (ret instanceof Promise) {
55+
ret.then(() => {
56+
if (delay !== null) {
57+
id = setTimeout(tick, delay);
58+
}
59+
});
60+
} else {
61+
if (delay !== null) {
62+
id = setTimeout(tick, delay);
63+
}
64+
}
65+
}
66+
if (delay !== null) {
67+
id = setTimeout(tick, delay);
68+
return () => id && clearTimeout(id);
69+
}
70+
}, [delay]);
71+
};
72+
73+
export const useFetchRecords = ({
74+
hotRef,
75+
isPageVisible,
76+
appId,
77+
query,
78+
}: {
79+
hotRef: React.RefObject<HotTable>;
80+
isPageVisible: boolean;
81+
appId: number;
82+
query: string;
83+
}): ReturnType<typeof useAsyncFn> => {
84+
return useAsyncFn(async (): Promise<void> => {
85+
const hot = hotRef.current?.hotInstance ?? undefined;
86+
if (!hot || !isPageVisible) return;
87+
const { records } = await client.record.getRecords({ app: appId, query });
88+
hot.loadData(records);
89+
}, [hotRef, appId, query, isPageVisible]);
90+
};
91+
92+
export const useOnChangeCheckbox = ({
93+
hotRef,
94+
appId,
95+
}: {
96+
hotRef: React.RefObject<HotTable>;
97+
appId: number;
98+
}): ReturnType<typeof useAsyncFn> => {
99+
return useAsyncFn(
100+
async (event: React.ChangeEvent<HTMLInputElement>, row: number) => {
101+
const hot = hotRef.current?.hotInstance ?? undefined;
102+
if (!hot) return;
103+
const sourceData = hot.getSourceData();
104+
const code = event.target.getAttribute('data-code') || '';
105+
const value = event.target.getAttribute('data-name') || '';
106+
const beforeValues = sourceData[row]?.[code]?.value as string[];
107+
const nextValues = (() => {
108+
// すでに値をもっているか
109+
const exsitedValueIndex = beforeValues.findIndex((v) => v === value);
110+
if (exsitedValueIndex < 0) {
111+
if (event.target.checked) {
112+
return [...beforeValues, value];
113+
} else {
114+
return [...beforeValues];
115+
}
116+
} else {
117+
if (event.target.checked) {
118+
return [...beforeValues];
119+
} else {
120+
return [...beforeValues.slice(0, exsitedValueIndex), ...beforeValues.slice(exsitedValueIndex + 1)];
121+
}
122+
}
123+
})();
124+
125+
// TODO: 本体側と共通化
126+
await client.bulkRequest({
127+
requests: [
128+
{
129+
method: 'PUT',
130+
api: '/k/v1/records.json',
131+
payload: {
132+
app: appId,
133+
records:
134+
sourceData[row]?.$id?.value != null
135+
? [
136+
{
137+
id: sourceData[row].$id.value,
138+
record: {
139+
...shapingRecord(excludeNonEditableFields(sourceData[row])),
140+
[code]: { value: nextValues },
141+
},
142+
},
143+
]
144+
: [],
145+
},
146+
},
147+
{
148+
method: 'POST',
149+
api: '/k/v1/records.json',
150+
payload: {
151+
app: appId,
152+
records:
153+
sourceData[row]?.$id?.value == null
154+
? [{ ...shapingRecord(excludeNonEditableFields(sourceData[row])), [code]: { value: nextValues } }]
155+
: [],
156+
},
157+
},
158+
],
159+
});
160+
},
161+
[appId, hotRef],
162+
);
163+
};
164+
165+
export const useAfterChange = ({
166+
hotRef,
167+
appId,
168+
}: {
169+
hotRef: React.RefObject<HotTable>;
170+
appId: number;
171+
}): ReturnType<typeof useAsyncFn> => {
172+
return useAsyncFn(
173+
async (changes: Handsontable.CellChange[] | null, source: Handsontable.ChangeSource) => {
174+
const hot = hotRef.current?.hotInstance ?? undefined;
175+
if (!hot) return;
176+
177+
const sourceData = hot.getSourceData();
178+
179+
// データ読み込み時はイベントを終了
180+
if (source === 'loadData' || !changes) return;
181+
182+
const changedRows = changes.map((row) => row[0]).filter((x, i, arr) => arr.indexOf(x) === i);
183+
184+
// FIXME: ここらへんはSourceData起点がいいかもしれない
185+
const insertRecords = changedRows
186+
.filter((row) => !sourceData[row]?.$id?.value)
187+
.map((row) => shapingRecord(excludeNonEditableFields(sourceData[row])));
188+
189+
const updateRecords = changedRows
190+
.filter((row) => sourceData[row]?.$id?.value)
191+
.map((row) => ({
192+
id: sourceData[row].$id.value,
193+
record: shapingRecord(excludeNonEditableFields(sourceData[row])),
194+
}));
195+
196+
return client.bulkRequest({
197+
requests: [
198+
{
199+
method: 'PUT',
200+
api: '/k/v1/records.json',
201+
payload: {
202+
app: appId,
203+
records: updateRecords,
204+
},
205+
},
206+
{
207+
method: 'POST',
208+
api: '/k/v1/records.json',
209+
payload: {
210+
app: appId,
211+
records: insertRecords,
212+
},
213+
},
214+
],
215+
});
216+
},
217+
[appId, hotRef],
218+
);
219+
};
220+
221+
export const useBeforeRemoveRow = ({
222+
hotRef,
223+
appId,
224+
}: {
225+
hotRef: React.RefObject<HotTable>;
226+
appId: number;
227+
}): ReturnType<typeof useAsyncFn> => {
228+
return useAsyncFn(
229+
async (index: number, amount: number) => {
230+
const hot = hotRef.current?.hotInstance ?? undefined;
231+
if (!hot) return;
232+
const sourceData = hot.getSourceData();
233+
const ids = sourceData.slice(index, index + amount).map((record) => record.$id.value);
234+
return await client.record.deleteRecords({ app: appId, ids });
235+
},
236+
[appId, hotRef],
237+
);
238+
};

0 commit comments

Comments
 (0)