ar-design 0.4.45 → 0.4.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,10 +9,14 @@ export type Config<T extends object> = {
9
9
  };
10
10
  perPage?: number;
11
11
  filter?: {
12
+ /**
13
+ * @deprecated *
14
+ */
12
15
  search?: (item: T, value: string) => boolean;
13
16
  keys: (item: T) => {
17
+ key: keyof T;
14
18
  name: string;
15
- key: string;
19
+ value: string;
16
20
  type: "select" | "date";
17
21
  }[];
18
22
  };
@@ -21,7 +25,7 @@ interface IProps<T extends object, TColumnProperties> {
21
25
  trackBy: (item: T) => string;
22
26
  columns: KanbanBoardColumnType<T, TColumnProperties>[];
23
27
  onChange?: (item: T, columnKey: string, columnProperties: TColumnProperties, hoverIndex: number) => void;
24
- onLazy?: (perPage: number) => void;
28
+ onLazyLoad?: (query: Record<string, string>, perPage: number, currentPage: number) => void;
25
29
  config?: Config<T>;
26
30
  }
27
31
  export default IProps;
@@ -4,8 +4,8 @@ import { Config } from "../IProps";
4
4
  interface IProps<T extends object> {
5
5
  states: {
6
6
  search: {
7
- get: string;
8
- set: Dispatch<React.SetStateAction<string>>;
7
+ get: string | null;
8
+ set: Dispatch<React.SetStateAction<string | null>>;
9
9
  };
10
10
  dateFilters: {
11
11
  get: Record<string, {
@@ -1,11 +1,13 @@
1
1
  "use client";
2
- import { useState } from "react";
2
+ import { useRef, useState } from "react";
3
3
  import DateFilters from "./DateFilters";
4
4
  import SelectFilters from "./SelectFilters";
5
5
  import React from "react";
6
6
  import Input from "../../../form/input";
7
7
  import { useTranslation } from "../../../../libs/core/application/hooks";
8
8
  function Filter({ states, config }) {
9
+ // refs
10
+ const _searchTimeOut = useRef(null);
9
11
  // states
10
12
  const [openName, setOpenName] = useState(null);
11
13
  // hooks
@@ -13,7 +15,13 @@ function Filter({ states, config }) {
13
15
  // methods
14
16
  const handleOpen = (name) => setOpenName(openName === name ? null : name);
15
17
  return (React.createElement("div", { className: "filters" },
16
- React.createElement(Input, { variant: "borderless", placeholder: t("KanbanBoard.Search.Input.Placeholder"), onChange: (event) => states.search.set(event.target.value.toLocaleLowerCase()) }),
18
+ React.createElement(Input, { variant: "borderless", placeholder: t("KanbanBoard.Search.Input.Placeholder"), onChange: (event) => {
19
+ if (_searchTimeOut.current)
20
+ clearTimeout(_searchTimeOut.current);
21
+ _searchTimeOut.current = setTimeout(() => {
22
+ states.search.set(event.target.value.toLocaleLowerCase());
23
+ }, 750);
24
+ } }),
17
25
  React.createElement("ul", null,
18
26
  React.createElement(DateFilters, { states: {
19
27
  dateFilters: {
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
2
  import IProps from "./IProps";
3
3
  import "../../../assets/css/components/data-display/kanban-board/styles.css";
4
- declare const KanbanBoard: <T extends object, TColumnProperties>({ trackBy, columns, onChange, onLazy, config, }: IProps<T, TColumnProperties>) => React.JSX.Element;
4
+ declare const KanbanBoard: <T extends object, TColumnProperties>({ trackBy, columns, onChange, onLazyLoad, config, }: IProps<T, TColumnProperties>) => React.JSX.Element;
5
5
  export default KanbanBoard;
@@ -1,23 +1,28 @@
1
1
  "use client";
2
- import React, { useEffect, useMemo, useRef, useState } from "react";
2
+ import React, { useCallback, useEffect, useRef, useState } from "react";
3
3
  import "../../../assets/css/components/data-display/kanban-board/styles.css";
4
4
  import DnD from "../dnd";
5
5
  import { ARIcon } from "../../icons";
6
6
  import Filter from "./filter";
7
- const KanbanBoard = function ({ trackBy, columns, onChange, onLazy, config, }) {
7
+ const KanbanBoard = function ({ trackBy, columns, onChange, onLazyLoad, config, }) {
8
8
  // refs
9
9
  const _kanbanWrapper = useRef(null);
10
10
  const _kanbanItems = useRef({});
11
11
  const _hoverItemIndex = useRef(null);
12
+ const _isProgrammaticScroll = useRef(false);
12
13
  const _scrollInterval = useRef(null);
13
14
  const _scrollAnimationFrame = useRef(null);
14
15
  const _scrollSpeedRef = useRef(0);
15
16
  const _lastScrollTop = useRef(0);
17
+ const _lastRequest = useRef("");
16
18
  // states
17
19
  const [data, setData] = useState([]);
18
- const [perPage, setPerPage] = useState(config?.perPage ?? 10);
20
+ // states -> Lazy Load
21
+ const [query, setQuery] = useState(null);
22
+ const [perPage] = useState(config?.perPage ?? 10);
23
+ const [currentPage, setCurrentPage] = useState(1);
19
24
  // states -> Filters
20
- const [search, setSearch] = useState("");
25
+ const [search, setSearch] = useState(null);
21
26
  const [selectFilters, setSelectFilters] = useState({});
22
27
  const [selectedFilters, setSelectedFilters] = useState({});
23
28
  const [dateFilters, setDateFilters] = useState({});
@@ -106,20 +111,22 @@ const KanbanBoard = function ({ trackBy, columns, onChange, onLazy, config, }) {
106
111
  if (item.classList.contains("dragging"))
107
112
  item.classList.remove("dragging");
108
113
  };
109
- const handleLazyScroll = useMemo(() => {
110
- return (event) => {
111
- const target = event.currentTarget;
112
- const { scrollTop, scrollHeight, clientHeight } = target;
113
- const isVerticalScroll = scrollTop !== _lastScrollTop.current;
114
- _lastScrollTop.current = scrollTop;
115
- if (!isVerticalScroll)
116
- return;
117
- const isBottom = scrollHeight - scrollTop <= clientHeight;
118
- if (isBottom) {
119
- setPerPage((prev) => prev + Number(config?.perPage ?? 10));
120
- }
121
- };
122
- }, [config?.perPage]);
114
+ const handleLazyLoadScroll = useCallback((event) => {
115
+ const target = event.currentTarget;
116
+ const { scrollTop, scrollHeight, clientHeight } = target;
117
+ if (_isProgrammaticScroll.current) {
118
+ if (scrollTop <= 5)
119
+ _isProgrammaticScroll.current = false;
120
+ return;
121
+ }
122
+ const isScrollingDown = scrollTop > _lastScrollTop.current;
123
+ _lastScrollTop.current = scrollTop;
124
+ if (!isScrollingDown)
125
+ return;
126
+ const isBottom = scrollHeight - scrollTop <= clientHeight;
127
+ if (isBottom)
128
+ setCurrentPage((prev) => prev + 1);
129
+ }, []);
123
130
  const handleStartScroll = (direction) => {
124
131
  const el = _kanbanWrapper.current;
125
132
  if (!el)
@@ -154,6 +161,11 @@ const KanbanBoard = function ({ trackBy, columns, onChange, onLazy, config, }) {
154
161
  };
155
162
  // useEffects
156
163
  useEffect(() => {
164
+ let _data = columns.map((col) => ({
165
+ ...col,
166
+ items: [...col.items],
167
+ }));
168
+ setData(_data);
157
169
  const selectMap = new Map();
158
170
  const dateMap = new Map();
159
171
  columns.forEach((col) => {
@@ -163,7 +175,7 @@ const KanbanBoard = function ({ trackBy, columns, onChange, onLazy, config, }) {
163
175
  if (k.type === "select") {
164
176
  if (!selectMap.has(k.name))
165
177
  selectMap.set(k.name, new Set());
166
- selectMap.get(k.name).add(k.key ?? null);
178
+ selectMap.get(k.name).add(k.value ?? null);
167
179
  }
168
180
  if (k.type === "date")
169
181
  dateMap.set(k.name, true);
@@ -181,86 +193,68 @@ const KanbanBoard = function ({ trackBy, columns, onChange, onLazy, config, }) {
181
193
  return next;
182
194
  });
183
195
  }, [columns]);
196
+ const _prevFilters = useRef(JSON.stringify({ search, selectedFilters, dateFilters }));
184
197
  useEffect(() => {
185
- let nextData = columns.map((col) => ({
186
- ...col,
187
- items: [...col.items],
188
- }));
189
- let firstMatchedId = null;
190
- // Search varsa...
191
- if (config?.filter?.search && search?.trim()) {
192
- const q = search.trim();
193
- nextData = nextData.map((col) => {
194
- const filteredItems = col.items.filter((item) => {
195
- const isMatch = config.filter.search(item, q);
196
- if (isMatch && !firstMatchedId)
197
- firstMatchedId = trackBy(item);
198
- return isMatch;
199
- });
200
- return { ...col, items: filteredItems };
201
- });
202
- }
203
- else {
204
- if (config?.filter?.search)
205
- config.filter.search({}, "");
206
- }
207
- // Select ve Date varsa...
208
- nextData = nextData.map((col) => ({
209
- ...col,
210
- items: col.items.filter((item) => {
211
- const keys = config?.filter?.keys(item) ?? [];
212
- // Select (checkbox)
213
- const selectOk = Object.entries(selectedFilters).every(([filterName, selectedSet]) => {
214
- if (!selectedSet || selectedSet.size === 0)
215
- return true;
216
- const value = keys.find((k) => k.name === filterName)?.key ?? null;
217
- return selectedSet.has(value);
218
- });
219
- if (!selectOk)
220
- return false;
221
- // Date (range)
222
- const dateOk = Object.entries(dateFilters).every(([filterName, range]) => {
223
- if (!range.from && !range.to)
224
- return true;
225
- const raw = keys.find((k) => k.name === filterName)?.key;
226
- if (!raw)
227
- return false;
228
- const d = new Date(raw);
229
- if (range.from && d < range.from)
230
- return false;
231
- if (range.to && d > range.to)
232
- return false;
233
- return true;
234
- });
235
- return dateOk;
236
- }),
237
- }));
238
- setData(nextData);
239
- // Focus / Scroll Control
240
- if (search?.trim() && firstMatchedId) {
241
- const element = _kanbanItems.current[firstMatchedId];
242
- const container = _kanbanWrapper.current;
243
- if (element && container) {
244
- const elementRect = element.getBoundingClientRect();
245
- const containerRect = container.getBoundingClientRect();
246
- // Senin hesaplaman: Mevcut scroll + elemanın container'a göre konumu - ofset değerlerin
247
- const scrollLeft = container.scrollLeft + (elementRect.left - containerRect.left) - 17.5;
248
- const scrollTop = container.scrollTop + (elementRect.top - containerRect.top) - 100;
249
- container.scrollTo({
250
- left: scrollLeft,
251
- top: scrollTop,
198
+ const normalizedFilters = JSON.stringify({
199
+ search,
200
+ selectedFilters: Object.fromEntries(Object.entries(selectedFilters).map(([k, v]) => [k, Array.from(v).sort()])),
201
+ dateFilters,
202
+ });
203
+ const hasSelectedFilters = Object.values(selectedFilters).some((set) => set.size > 0);
204
+ const hasDateFilters = Object.values(dateFilters).some((r) => r.from || r.to);
205
+ // Page reset + Scroll logic.
206
+ if (_prevFilters.current !== normalizedFilters) {
207
+ setCurrentPage(1);
208
+ _prevFilters.current = normalizedFilters;
209
+ if (_kanbanWrapper.current) {
210
+ _isProgrammaticScroll.current = true;
211
+ _kanbanWrapper.current.scrollTo({
212
+ top: 0,
213
+ left: 0,
252
214
  behavior: "smooth",
253
215
  });
254
216
  }
255
217
  }
256
- // else if (!search?.trim() && _kanbanWrapper.current) {
257
- // _kanbanWrapper.current.scrollTo({ top: 0, left: 0, behavior: "smooth" });
258
- // }
259
- }, [columns, search, selectedFilters, dateFilters]);
218
+ // Query Build Logic.
219
+ if (!search && !hasSelectedFilters && !hasDateFilters) {
220
+ setQuery(null);
221
+ return;
222
+ }
223
+ const sampleItem = columns?.[0]?.items?.[0];
224
+ const keys = config?.filter?.keys(sampleItem) ?? [];
225
+ const keyMap = Object.fromEntries(keys.map((k) => [k.name, k.key]));
226
+ const dateQuery = Object.entries(dateFilters).reduce((acc, [name, range]) => {
227
+ if (range.from || range.to) {
228
+ const technicalKey = keyMap[name] || name;
229
+ acc[technicalKey] = {
230
+ from: range.from,
231
+ to: range.to,
232
+ };
233
+ }
234
+ return acc;
235
+ }, {});
236
+ const selectQuery = Object.entries(selectedFilters).reduce((acc, [name, set]) => {
237
+ if (set.size > 0) {
238
+ const technicalKey = keyMap[name] || name;
239
+ acc[technicalKey] = Array.from(set);
240
+ }
241
+ return acc;
242
+ }, {});
243
+ setQuery({
244
+ keyword: search ?? "",
245
+ ...dateQuery,
246
+ ...selectQuery,
247
+ });
248
+ }, [search, selectedFilters, dateFilters, columns, config]);
260
249
  useEffect(() => {
261
- if (onLazy)
262
- onLazy(perPage);
263
- }, [perPage, onLazy]);
250
+ if (!onLazyLoad)
251
+ return;
252
+ const key = JSON.stringify({ query, currentPage, perPage });
253
+ if (_lastRequest.current === key)
254
+ return;
255
+ _lastRequest.current = key;
256
+ onLazyLoad(query, perPage, currentPage);
257
+ }, [query, currentPage, perPage, onLazyLoad]);
264
258
  return (React.createElement(React.Fragment, null,
265
259
  config?.filter && (React.createElement(Filter, { states: {
266
260
  search: {
@@ -282,7 +276,7 @@ const KanbanBoard = function ({ trackBy, columns, onChange, onLazy, config, }) {
282
276
  }, config: config })),
283
277
  React.createElement("div", { ref: _kanbanWrapper, className: "ar-kanban-board", style: {
284
278
  height: `calc(100dvh - (${_kanbanWrapper.current?.getBoundingClientRect().top}px + ${config?.safeAreaOffset?.bottom ?? 0}px))`,
285
- }, onScroll: handleLazyScroll, onDragOver: handleBoardDragOver, onDragEnd: stopScrolling, onDrop: stopScrolling },
279
+ }, onScroll: handleLazyLoadScroll, onDragOver: handleBoardDragOver, onDragEnd: stopScrolling, onDrop: stopScrolling },
286
280
  React.createElement("div", { className: "buttons" },
287
281
  React.createElement("div", { className: "button left", onMouseDown: () => handleStartScroll("left"), onMouseUp: handleStopScroll, onMouseLeave: handleStopScroll },
288
282
  React.createElement(ARIcon, { icon: "ArrowLeft" })),
@@ -308,7 +302,7 @@ const KanbanBoard = function ({ trackBy, columns, onChange, onLazy, config, }) {
308
302
  const rect = event.currentTarget.getBoundingClientRect();
309
303
  const mouseY = event.clientY;
310
304
  const isBelow = mouseY > rect.top + rect.height / 2;
311
- _hoverItemIndex.current = isBelow ? index + 1 : index;
305
+ _hoverItemIndex.current = isBelow ? dndIndex + 1 : dndIndex;
312
306
  } }, board.renderItem(item, index)));
313
307
  }, columnKey: board.key, confing: { isMoveIcon: false } })))))))));
314
308
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ar-design",
3
- "version": "0.4.45",
3
+ "version": "0.4.47",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",