@craftainer/crud-views 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -52,6 +52,28 @@ const client = createClient({ baseUrl: "https://api.example.com" });
52
52
  </CrudProvider>;
53
53
  ```
54
54
 
55
+ ### Inline edit and ETags
56
+
57
+ The list endpoint returns no ETags, so the table's inline edit takes
58
+ `ifMatch(row)` from the app. It may return a string, `undefined` (the PATCH
59
+ goes without `If-Match`) or a promise of either, the same shape as
60
+ `ResourceForm`'s `etag`. `fetchEtagById` stands for the app's own lookup, for
61
+ example fetching the row through the SDK and reading its cached ETag:
62
+
63
+ ```tsx
64
+ <ResourceList
65
+ resource="heroes"
66
+ editable
67
+ ifMatch={async (row) =>
68
+ (await fetchEtagById(row["id"] as number)) ?? undefined
69
+ }
70
+ />
71
+ ```
72
+
73
+ The edit waits for the promise and stays in a saving state (the cell stays
74
+ in edit mode and the table is `aria-busy`); a rejection sends nothing and shows "Could not save: ..." like
75
+ any other save error.
76
+
55
77
  ## Development
56
78
 
57
79
  Open the repository in the devcontainer, then run
package/dist/index.d.mts CHANGED
@@ -958,9 +958,12 @@ interface DataTableListProps {
958
958
  * The ETag of a row, sent as `If-Match` on an inline edit (a stale one fails
959
959
  * with 412 and the view reloads the row). The list endpoint returns no ETags,
960
960
  * so the app supplies them (for example from its SDK cache); without this
961
- * prop the edit is unconditional.
961
+ * prop the edit is unconditional. May return a promise (same shape as
962
+ * `ResourceFormProps.etag`): the edit waits for it, stays in a saving state
963
+ * meanwhile, and is not sent if it rejects (the error is shown like any save
964
+ * error). `undefined` sends the PATCH without `If-Match`.
962
965
  */
963
- readonly ifMatch?: ((row: Row$1) => string | undefined) | undefined;
966
+ readonly ifMatch?: ((row: Row$1) => string | undefined | Promise<string | undefined>) | undefined;
964
967
  readonly pageSizeOptions?: readonly number[] | undefined;
965
968
  readonly onRowClick?: ((row: Row$1) => void) | undefined;
966
969
  readonly renderers?: RendererOverrides | undefined;
@@ -1003,7 +1006,8 @@ interface ResourceListProps {
1003
1006
  readonly titleField?: string | undefined;
1004
1007
  /** Table only: edit cells in place. */
1005
1008
  readonly editable?: boolean | undefined;
1006
- readonly ifMatch?: ((row: Row) => string | undefined) | undefined;
1009
+ /** Table only: a row's ETag for inline edits; may be async. See `DataTableListProps.ifMatch`. */
1010
+ readonly ifMatch?: ((row: Row) => string | undefined | Promise<string | undefined>) | undefined;
1007
1011
  readonly sortableOnly?: boolean | undefined;
1008
1012
  /** Show the sort dropdown. Default: for the card variants, which have no header to click. */
1009
1013
  readonly sortDropdown?: boolean | undefined;
package/dist/index.mjs CHANGED
@@ -4016,6 +4016,7 @@ function DataTableList({ ready, state, columns, sortableOnly = false, configurab
4016
4016
  const update = ready.hooks.useUpdate();
4017
4017
  const [hidden, setHidden] = useState(/* @__PURE__ */ new Set());
4018
4018
  const [notice, setNotice] = useState(null);
4019
+ const [saving, setSaving] = useState(0);
4019
4020
  const shown = useMemo(() => resolveColumns(ready, columns).filter((c) => !sortableOnly || c.field.sortable), [
4020
4021
  ready,
4021
4022
  columns,
@@ -4072,14 +4073,19 @@ function DataTableList({ ready, state, columns, sortableOnly = false, configurab
4072
4073
  if (canEdit(field, editable, idField) && value !== void 0 && !same(value, previous[field.name])) patch[field.name] = value;
4073
4074
  }
4074
4075
  if (Object.keys(patch).length === 0) return previous;
4075
- const tag = ifMatch?.(previous);
4076
- const saved = await update.mutateAsync({
4077
- id: previous[idField],
4078
- patch,
4079
- ...tag === void 0 ? {} : { ifMatch: tag }
4080
- });
4081
- setNotice(null);
4082
- return saved;
4076
+ setSaving((n) => n + 1);
4077
+ try {
4078
+ const tag = await ifMatch?.(previous);
4079
+ const saved = await update.mutateAsync({
4080
+ id: previous[idField],
4081
+ patch,
4082
+ ...tag === void 0 ? {} : { ifMatch: tag }
4083
+ });
4084
+ setNotice(null);
4085
+ return saved;
4086
+ } finally {
4087
+ setSaving((n) => n - 1);
4088
+ }
4083
4089
  }
4084
4090
  function saveFailed(error) {
4085
4091
  if (error instanceof PreconditionFailed) {
@@ -4092,51 +4098,54 @@ function DataTableList({ ready, state, columns, sortableOnly = false, configurab
4092
4098
  error: list.error,
4093
4099
  onRetry: () => void list.refetch()
4094
4100
  });
4095
- return /* @__PURE__ */ jsxs(Box, { children: [
4096
- configurableColumns && /* @__PURE__ */ jsx(ColumnsMenu, {
4097
- columns: shown,
4098
- hidden,
4099
- onChange: setHidden
4100
- }),
4101
- notice !== null && /* @__PURE__ */ jsx(Alert, {
4102
- severity: "warning",
4103
- onClose: () => setNotice(null),
4104
- sx: { mb: 1 },
4105
- children: notice
4106
- }),
4107
- /* @__PURE__ */ jsx(DataGrid, {
4108
- "aria-label": resource.title,
4109
- autoHeight: true,
4110
- rows,
4111
- columns: gridColumns,
4112
- getRowId: (row) => row[idField],
4113
- loading: list.isFetching,
4114
- columnVisibilityModel: visibility,
4115
- paginationMode: "server",
4116
- sortingMode: "server",
4117
- filterMode: "server",
4118
- rowCount,
4119
- localeText: { paginationDisplayedRows: ({ from, to }) => hasNextPage ? `${from}–${to}` : `${from}–${to} of ${rowCount}` },
4120
- paginationModel: {
4121
- page: page - 1,
4122
- pageSize
4123
- },
4124
- pageSizeOptions: sizes,
4125
- onPaginationModelChange: (model) => model.pageSize === pageSize ? state.setPage(model.page + 1) : state.setPageSize(model.pageSize),
4126
- sortModel,
4127
- onSortModelChange: (model) => state.setSort(model.map((m) => ({
4128
- field: m.field,
4129
- desc: m.sort === "desc"
4130
- }))),
4131
- disableColumnFilter: true,
4132
- disableColumnMenu: true,
4133
- disableRowSelectionOnClick: true,
4134
- processRowUpdate: save,
4135
- onProcessRowUpdateError: saveFailed,
4136
- onRowClick: (p) => onRowClick?.(p.row),
4137
- ...gridProps
4138
- })
4139
- ] });
4101
+ return /* @__PURE__ */ jsxs(Box, {
4102
+ "aria-busy": saving > 0,
4103
+ children: [
4104
+ configurableColumns && /* @__PURE__ */ jsx(ColumnsMenu, {
4105
+ columns: shown,
4106
+ hidden,
4107
+ onChange: setHidden
4108
+ }),
4109
+ notice !== null && /* @__PURE__ */ jsx(Alert, {
4110
+ severity: "warning",
4111
+ onClose: () => setNotice(null),
4112
+ sx: { mb: 1 },
4113
+ children: notice
4114
+ }),
4115
+ /* @__PURE__ */ jsx(DataGrid, {
4116
+ "aria-label": resource.title,
4117
+ autoHeight: true,
4118
+ rows,
4119
+ columns: gridColumns,
4120
+ getRowId: (row) => row[idField],
4121
+ loading: list.isFetching,
4122
+ columnVisibilityModel: visibility,
4123
+ paginationMode: "server",
4124
+ sortingMode: "server",
4125
+ filterMode: "server",
4126
+ rowCount,
4127
+ localeText: { paginationDisplayedRows: ({ from, to }) => hasNextPage ? `${from}–${to}` : `${from}–${to} of ${rowCount}` },
4128
+ paginationModel: {
4129
+ page: page - 1,
4130
+ pageSize
4131
+ },
4132
+ pageSizeOptions: sizes,
4133
+ onPaginationModelChange: (model) => model.pageSize === pageSize ? state.setPage(model.page + 1) : state.setPageSize(model.pageSize),
4134
+ sortModel,
4135
+ onSortModelChange: (model) => state.setSort(model.map((m) => ({
4136
+ field: m.field,
4137
+ desc: m.sort === "desc"
4138
+ }))),
4139
+ disableColumnFilter: true,
4140
+ disableColumnMenu: true,
4141
+ disableRowSelectionOnClick: true,
4142
+ processRowUpdate: save,
4143
+ onProcessRowUpdateError: saveFailed,
4144
+ onRowClick: (p) => onRowClick?.(p.row),
4145
+ ...gridProps
4146
+ })
4147
+ ]
4148
+ });
4140
4149
  }
4141
4150
  //#endregion
4142
4151
  //#region src/list/LoadMoreList.tsx