ar-design 0.4.44 → 0.4.46

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.
@@ -7,11 +7,16 @@ export type Config<T extends object> = {
7
7
  bottom?: number;
8
8
  left?: number;
9
9
  };
10
+ perPage?: number;
10
11
  filter?: {
12
+ /**
13
+ * @deprecated *
14
+ */
11
15
  search?: (item: T, value: string) => boolean;
12
16
  keys: (item: T) => {
17
+ key: keyof T;
13
18
  name: string;
14
- key: string;
19
+ value: string;
15
20
  type: "select" | "date";
16
21
  }[];
17
22
  };
@@ -20,6 +25,7 @@ interface IProps<T extends object, TColumnProperties> {
20
25
  trackBy: (item: T) => string;
21
26
  columns: KanbanBoardColumnType<T, TColumnProperties>[];
22
27
  onChange?: (item: T, columnKey: string, columnProperties: TColumnProperties, hoverIndex: number) => void;
28
+ onLazyLoad?: (query: Record<string, string>, perPage: number, currentPage: number) => void;
23
29
  config?: Config<T>;
24
30
  }
25
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, 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,21 +1,28 @@
1
1
  "use client";
2
- import React, { useEffect, 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, 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
- const _scrollSpeedRef = useRef(0); // px/frame
15
+ const _scrollSpeedRef = useRef(0);
16
+ const _lastScrollTop = useRef(0);
17
+ const _lastRequest = useRef("");
15
18
  // states
16
19
  const [data, setData] = useState([]);
20
+ // states -> Lazy Load
21
+ const [query, setQuery] = useState(null);
22
+ const [perPage] = useState(config?.perPage ?? 10);
23
+ const [currentPage, setCurrentPage] = useState(1);
17
24
  // states -> Filters
18
- const [search, setSearch] = useState("");
25
+ const [search, setSearch] = useState(null);
19
26
  const [selectFilters, setSelectFilters] = useState({});
20
27
  const [selectedFilters, setSelectedFilters] = useState({});
21
28
  const [dateFilters, setDateFilters] = useState({});
@@ -104,6 +111,22 @@ const KanbanBoard = function ({ trackBy, columns, onChange, config, }) {
104
111
  if (item.classList.contains("dragging"))
105
112
  item.classList.remove("dragging");
106
113
  };
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
+ }, []);
107
130
  const handleStartScroll = (direction) => {
108
131
  const el = _kanbanWrapper.current;
109
132
  if (!el)
@@ -138,6 +161,11 @@ const KanbanBoard = function ({ trackBy, columns, onChange, config, }) {
138
161
  };
139
162
  // useEffects
140
163
  useEffect(() => {
164
+ let _data = columns.map((col) => ({
165
+ ...col,
166
+ items: [...col.items],
167
+ }));
168
+ setData(_data);
141
169
  const selectMap = new Map();
142
170
  const dateMap = new Map();
143
171
  columns.forEach((col) => {
@@ -147,7 +175,7 @@ const KanbanBoard = function ({ trackBy, columns, onChange, config, }) {
147
175
  if (k.type === "select") {
148
176
  if (!selectMap.has(k.name))
149
177
  selectMap.set(k.name, new Set());
150
- selectMap.get(k.name).add(k.key ?? null);
178
+ selectMap.get(k.name).add(k.value ?? null);
151
179
  }
152
180
  if (k.type === "date")
153
181
  dateMap.set(k.name, true);
@@ -165,82 +193,61 @@ const KanbanBoard = function ({ trackBy, columns, onChange, config, }) {
165
193
  return next;
166
194
  });
167
195
  }, [columns]);
196
+ const _prevFilters = useRef(JSON.stringify({ search, selectedFilters, dateFilters }));
168
197
  useEffect(() => {
169
- let nextData = columns.map((col) => ({
170
- ...col,
171
- items: [...col.items],
172
- }));
173
- let firstMatchedId = null;
174
- // Search varsa...
175
- if (config?.filter?.search && search?.trim()) {
176
- const q = search.trim();
177
- nextData = nextData.map((col) => {
178
- const filteredItems = col.items.filter((item) => {
179
- const isMatch = config.filter.search(item, q);
180
- if (isMatch && !firstMatchedId)
181
- firstMatchedId = trackBy(item);
182
- return isMatch;
183
- });
184
- return { ...col, items: filteredItems };
185
- });
186
- }
187
- else {
188
- if (config?.filter?.search)
189
- config.filter.search({}, "");
190
- }
191
- // Select ve Date varsa...
192
- nextData = nextData.map((col) => ({
193
- ...col,
194
- items: col.items.filter((item) => {
195
- const keys = config?.filter?.keys(item) ?? [];
196
- // Select (checkbox)
197
- const selectOk = Object.entries(selectedFilters).every(([filterName, selectedSet]) => {
198
- if (!selectedSet || selectedSet.size === 0)
199
- return true;
200
- const value = keys.find((k) => k.name === filterName)?.key ?? null;
201
- return selectedSet.has(value);
202
- });
203
- if (!selectOk)
204
- return false;
205
- // Date (range)
206
- const dateOk = Object.entries(dateFilters).every(([filterName, range]) => {
207
- if (!range.from && !range.to)
208
- return true;
209
- const raw = keys.find((k) => k.name === filterName)?.key;
210
- if (!raw)
211
- return false;
212
- const d = new Date(raw);
213
- if (range.from && d < range.from)
214
- return false;
215
- if (range.to && d > range.to)
216
- return false;
217
- return true;
218
- });
219
- return dateOk;
220
- }),
221
- }));
222
- setData(nextData);
223
- // Focus / Scroll Control
224
- if (search?.trim() && firstMatchedId) {
225
- const element = _kanbanItems.current[firstMatchedId];
226
- const container = _kanbanWrapper.current;
227
- if (element && container) {
228
- const elementRect = element.getBoundingClientRect();
229
- const containerRect = container.getBoundingClientRect();
230
- // Senin hesaplaman: Mevcut scroll + elemanın container'a göre konumu - ofset değerlerin
231
- const scrollLeft = container.scrollLeft + (elementRect.left - containerRect.left) - 17.5;
232
- const scrollTop = container.scrollTop + (elementRect.top - containerRect.top) - 100;
233
- container.scrollTo({
234
- left: scrollLeft,
235
- top: scrollTop,
236
- behavior: "smooth",
237
- });
198
+ const currentFilters = JSON.stringify({
199
+ search,
200
+ selectedFilters: Object.fromEntries(Object.entries(selectedFilters).map(([k, v]) => [k, Array.from(v)])),
201
+ dateFilters,
202
+ });
203
+ if (_prevFilters.current !== currentFilters) {
204
+ setCurrentPage(1);
205
+ _prevFilters.current = currentFilters;
206
+ if (_kanbanWrapper.current) {
207
+ _isProgrammaticScroll.current = true;
208
+ _kanbanWrapper.current.scrollTo({ top: 0, left: 0, behavior: "smooth" });
238
209
  }
239
210
  }
240
- else if (!search?.trim() && _kanbanWrapper.current) {
241
- _kanbanWrapper.current.scrollTo({ top: 0, left: 0, behavior: "smooth" });
211
+ }, [search, selectedFilters, dateFilters]);
212
+ useEffect(() => {
213
+ if (!search && Object.keys(selectedFilters).length === 0 && Object.keys(dateFilters).length === 0) {
214
+ setQuery(null);
215
+ return;
242
216
  }
243
- }, [columns, search, selectedFilters, dateFilters]);
217
+ const sampleItem = columns[0]?.items[0];
218
+ const keys = sampleItem ? (config?.filter?.keys(sampleItem) ?? []) : [];
219
+ const dateQuery = Object.entries(dateFilters).reduce((acc, [name, range]) => {
220
+ if (range.from || range.to) {
221
+ const technicalKey = keys.find((k) => k.name === name)?.key || name;
222
+ acc[technicalKey] = {
223
+ from: range.from,
224
+ to: range.to,
225
+ };
226
+ }
227
+ return acc;
228
+ }, {});
229
+ const selectQuery = Object.entries(selectedFilters).reduce((acc, [filterName, selectedSet]) => {
230
+ if (selectedSet && selectedSet.size > 0) {
231
+ const technicalKey = keys.find((k) => k.name === filterName)?.key || filterName;
232
+ acc[technicalKey] = Array.from(selectedSet);
233
+ }
234
+ return acc;
235
+ }, {});
236
+ setQuery({
237
+ keyword: search ?? "",
238
+ ...dateQuery,
239
+ ...selectQuery,
240
+ });
241
+ }, [search, selectedFilters, dateFilters]);
242
+ useEffect(() => {
243
+ if (!onLazyLoad)
244
+ return;
245
+ const key = JSON.stringify({ query, currentPage, perPage });
246
+ if (_lastRequest.current === key)
247
+ return;
248
+ _lastRequest.current = key;
249
+ onLazyLoad(query, perPage, currentPage);
250
+ }, [query, currentPage, perPage, onLazyLoad]);
244
251
  return (React.createElement(React.Fragment, null,
245
252
  config?.filter && (React.createElement(Filter, { states: {
246
253
  search: {
@@ -262,7 +269,7 @@ const KanbanBoard = function ({ trackBy, columns, onChange, config, }) {
262
269
  }, config: config })),
263
270
  React.createElement("div", { ref: _kanbanWrapper, className: "ar-kanban-board", style: {
264
271
  height: `calc(100dvh - (${_kanbanWrapper.current?.getBoundingClientRect().top}px + ${config?.safeAreaOffset?.bottom ?? 0}px))`,
265
- }, onDragOver: handleBoardDragOver, onDragEnd: stopScrolling, onDrop: stopScrolling },
272
+ }, onScroll: handleLazyLoadScroll, onDragOver: handleBoardDragOver, onDragEnd: stopScrolling, onDrop: stopScrolling },
266
273
  React.createElement("div", { className: "buttons" },
267
274
  React.createElement("div", { className: "button left", onMouseDown: () => handleStartScroll("left"), onMouseUp: handleStopScroll, onMouseLeave: handleStopScroll },
268
275
  React.createElement(ARIcon, { icon: "ArrowLeft" })),
@@ -288,7 +295,7 @@ const KanbanBoard = function ({ trackBy, columns, onChange, config, }) {
288
295
  const rect = event.currentTarget.getBoundingClientRect();
289
296
  const mouseY = event.clientY;
290
297
  const isBelow = mouseY > rect.top + rect.height / 2;
291
- _hoverItemIndex.current = isBelow ? index + 1 : index;
298
+ _hoverItemIndex.current = isBelow ? dndIndex + 1 : dndIndex;
292
299
  } }, board.renderItem(item, index)));
293
300
  }, columnKey: board.key, confing: { isMoveIcon: false } })))))))));
294
301
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ar-design",
3
- "version": "0.4.44",
3
+ "version": "0.4.46",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",