[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M699XWR8H754KWRTMZF45A29":3,"skill-same:01M699XWR8H754KWRTMZF45A29":76},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":26,"linkOnly":27,"markdownContent":28,"markdownBody":29,"frontMatter":30,"refName":31,"commitSha":32,"skillDirectory":33,"forksCount":34,"repositoryHtmlUrl":35,"repositoryHomepageUrl":6,"ownerHtmlUrl":36,"ownerBlogUrl":37,"ownerDescription":38,"updatedAt":39,"install":40,"resources":53},"01M699XWR8H754KWRTMZF45A29","crud-ui",null,"Use when a data-backed app needs a working interface — a list people can add to, edit, and delete from, with the states that make it feel finished. Pairs with data-modeling, which defines the entities; this covers the React side: optimistic updates, forms, loading \u002F empty \u002F error states, and delete confirmation. Triggers: CRUD, add edit delete, list, form, table, manage, tracker, admin screen, \"let me add items\", \"I want to edit them\", inline edit, optimistic.","microsoft\u002Frayfin","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M699VK9NEV8GH0Y5JKT1G8XG",661,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Fcrud-ui\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",[24,25],"crud ui","react","2026-11-10 10:12:30",false,"---\nname: crud-ui\ndescription: >\n  Use when a data-backed app needs a working interface — a list people can add to,\n  edit, and delete from, with the states that make it feel finished. Pairs with\n  data-modeling, which defines the entities; this covers the React side: optimistic\n  updates, forms, loading \u002F empty \u002F error states, and delete confirmation. Triggers:\n  CRUD, add edit delete, list, form, table, manage, tracker, admin screen, \"let me\n  add items\", \"I want to edit them\", inline edit, optimistic.\n---\n\n# CRUD UI — a list people can actually use\n\n`data-modeling` gives you entities and a typed client. This gives you the interface\nover them, and the states that separate a demo from something usable.\n\n**Prerequisites:** entities defined per `data-modeling`.\nKeep the template's dynamic sign-in flow and app-wide auth gate intact, including outside the portal.\n\n> **The client API is the package's, not this skill's.** For the query chain,\n> mutations, filtering, `findById`, and relationship writes, read\n> `node_modules\u002F@microsoft\u002Frayfin-guide\u002Fassets\u002Fdocs\u002Fdata\u002Fgraphql.md`. For paging,\n> cursors and text-field nullability, read\n> `node_modules\u002F@microsoft\u002Frayfin-data\u002Fassets\u002Fdocs\u002Findex.md`. Those move between\n> releases; the React patterns below do not.\n\nCopy the two files this skill ships into the app:\n\n```text\n.agents\u002Fskills\u002Fcrud-ui\u002Fkit\u002Fcomponents\u002FuseCrud.ts   ->  packages\u002Ffrontend\u002Fsrc\u002Fcomponents\u002FuseCrud.ts\n.agents\u002Fskills\u002Fcrud-ui\u002Fkit\u002Fcomponents\u002Fstates.tsx   ->  packages\u002Ffrontend\u002Fsrc\u002Fcomponents\u002Fstates.tsx\n```\n\n## The shape\n\n`useCrud` owns state and mutations; you own rendering. Give it four functions\nbound to your entity and it returns items plus optimistic `create` \u002F `update` \u002F\n`remove`. The four functions are ordinary client calls — see `data\u002Fgraphql.md`\nfor their exact signatures.\n\n```tsx\nimport { useCrud } from '@\u002Fcomponents\u002FuseCrud';\nimport { LoadingState, EmptyState, ErrorState } from '@\u002Fcomponents\u002Fstates';\nimport { getRayfinClient } from '@\u002Flib\u002Frayfin-client';\n\ninterface Task { id: string; title: string; done: boolean }\n\nexport function TaskList() {\n  const crud = useCrud\u003CTask>({\n    list: async () => (await getRayfinClient()).data.Task.select(['id', 'title', 'done']).execute(),\n    create: async (input) => (await getRayfinClient()).data.Task.create(input),\n    update: async (id, patch) => (await getRayfinClient()).data.Task.update({ id }, patch),\n    remove: async (id) => { await (await getRayfinClient()).data.Task.delete({ id }); },\n  });\n\n  if (crud.status === 'loading') return \u003CLoadingState \u002F>;\n  if (crud.status === 'error')   return \u003CErrorState message={crud.error!} onRetry={crud.refresh} \u002F>;\n\n  return (\n    \u003Cdiv className=\"space-y-3\">\n      {crud.error ? \u003CErrorState message={crud.error} onRetry={crud.clearError} \u002F> : null}\n\n      \u003CAddTaskForm\n        onAdd={(title) => { void crud.create({ title, done: false }); }}\n        disabled={crud.pending}\n      \u002F>\n\n      {crud.items.length === 0 ? (\n        \u003CEmptyState title=\"No tasks yet\" hint=\"Add your first one above.\" \u002F>\n      ) : (\n        \u003Cul className=\"space-y-2\">\n          {crud.items.map((task) => (\n            \u003Cli key={task.id} className=\"flex items-center gap-3 rounded-lg border border-black\u002F10 px-4 py-3 dark:border-white\u002F15\">\n              \u003Cinput\n                type=\"checkbox\"\n                checked={task.done}\n                onChange={(e) => crud.update(task.id, { done: e.target.checked })}\n              \u002F>\n              \u003Cspan className={task.done ? 'flex-1 line-through opacity-60' : 'flex-1'}>{task.title}\u003C\u002Fspan>\n              \u003Cbutton type=\"button\" onClick={() => crud.remove(task.id)}>Remove\u003C\u002Fbutton>\n            \u003C\u002Fli>\n          ))}\n        \u003C\u002Ful>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Why optimistic\n\nA Fabric round-trip is slow enough that the naive version feels broken — you click\nAdd, nothing happens, then a row appears. `useCrud` applies the change locally first\nand **rolls it back if the server rejects it**, surfacing the error. The UI tracks\nintent without lying about the outcome.\n\nTwo details worth knowing: a created row gets a temporary `pending-…` id so React\nkeys stay stable until the real row arrives, and a failed delete restores the row **in\nits original position**, not appended at the end.\n\n### Mutation result contract\n\nThe kit merges the row returned by create or update into local state.\nTreat that returned row as the service's authoritative representation, including normalized dates, rounded values, generated fields, and defaults.\nDo not refetch only to compare every field with the submitted input, and do not turn a successful mutation into a failure because the readback differs.\n\n`useCrud.create`, `useCrud.update`, and `useCrud.remove` catch operation errors, store the message in `crud.error`, roll back optimistic state where needed, and return `Promise\u003Cvoid>`.\nFulfillment of that wrapper promise is not proof that the save succeeded.\nUse `crud.pending`, `crud.error`, and the rendered item state for kit-driven UI feedback.\nWhen code calls a lower-level mutation directly, follow that operation's documented success and error contract instead of assuming that resolution or rejection means the same thing across APIs.\n\n## Forms\n\nControlled inputs, validation before submit, and a disabled button while pending:\n\n```tsx\nfunction AddTaskForm({ onAdd, disabled }: { onAdd(title: string): void; disabled?: boolean }) {\n  const [title, setTitle] = useState('');\n  const trimmed = title.trim();\n\n  return (\n    \u003Cform\n      onSubmit={(e) => { e.preventDefault(); if (!trimmed) return; onAdd(trimmed); }}\n      className=\"flex gap-2\"\n    >\n      \u003Clabel htmlFor=\"new-task\" className=\"sr-only\">Task\u003C\u002Flabel>\n      \u003Cinput\n        id=\"new-task\"\n        value={title}\n        onChange={(e) => setTitle(e.target.value)}\n        placeholder=\"Add a task\"\n        maxLength={200}\n        className=\"flex-1 rounded-lg border border-black\u002F15 px-3 py-2 dark:border-white\u002F20\"\n      \u002F>\n      \u003Cbutton type=\"submit\" disabled={disabled || !trimmed} className=\"rounded-lg px-4 py-2 font-medium disabled:opacity-50\">\n        Add\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}\n```\n\n- **Match the entity's constraints.** `@text({ max: 200 })` in the schema means\n  `maxLength={200}` here. Client validation that disagrees with the server produces\n  errors the user can't act on.\n- **Do not clear the field from an awaited `useCrud` wrapper alone.**\n  Its `Promise\u003Cvoid>` fulfills after either success or internally handled failure.\n  Keep the input on error, and clear it only from explicit success state or an operation contract that reports success directly.\n- **Every input needs a label** — `sr-only` when the design has no room for one.\n\n## Deleting\n\nUse `ConfirmDelete` for anything irreversible. It requires you to name the item,\nbecause \"Delete this item?\" is a prompt people click through without reading.\n\n```tsx\n{confirming === task.id ? (\n  \u003CConfirmDelete\n    label={task.title}\n    pending={crud.pending}\n    onCancel={() => setConfirming(null)}\n    onConfirm={() => { crud.remove(task.id); setConfirming(null); }}\n  \u002F>\n) : (\n  \u003Cbutton type=\"button\" onClick={() => setConfirming(task.id)}>Delete\u003C\u002Fbutton>\n)}\n```\n\nSkip confirmation only for genuinely cheap, reversible toggles.\n\n## Per-user data\n\nIf each person should see only their own rows, this is **server-side** work, not a\nfilter in the UI. Add the `@role` policy per `data-modeling` § per-user data — the\ndecorator and policy DSL are documented in\n`node_modules\u002F@microsoft\u002Frayfin-core\u002Fassets\u002Fdocs\u002Fpermissions.md` — and stamp the\nowner column from the session on create:\n\n```ts\nconst session = client.auth.getSession();\nif (!session.isAuthenticated || !session.user) throw new Error('Not signed in.');\ncreate: (input) => client.data.Task.create({ ...input, owner_id: session.user.id }),\n```\n\nThe column name has to match the one the policy compares against — the\n`data-modeling` pack seeds `owner_id`. Stamping a differently-named field leaves\nthe real one unset, and the policy then refuses every row.\n\n**Never** filter by owner only in the client — the rows would still be fetchable.\n\n## Seed data\n\nReal-feeling content on first run makes an empty app legible. Seed **once, on\ndemand** — never on every mount, which duplicates rows on refresh:\n\n```tsx\nasync function seedIfEmpty() {\n  const existing = await client.data.Task.select(['id']).execute();\n  if (existing.length > 0) return;\n  for (const title of ['Read the brief', 'Draft the plan']) {\n    await client.data.Task.create({\n      title,\n      done: false,\n      owner_id: session.user.id,\n    });\n  }\n}\n```\n\nPrefer wiring this to an explicit \"Add sample tasks\" button in the empty state, so it\nis the user's choice and obviously distinct from real content. Seeded rows are real\nrows in their database — if they are invented *figures* rather than starter content,\nlabel them visibly in the UI as sample data, per the Critical Rules in `AGENTS.md`.\n\n## Rendering a row safely\n\nTreat every value crossing into JSX as untrusted display input.\nA row written before a field existed may return `null` or `undefined`.\nA date can arrive as a `Date`, and objects or arrays are never valid React text without an app-specific conversion.\nDo not interpolate a raw `Date`, object, or array.\n\n(Optional vs. required text nullability is a package behaviour — see\n`node_modules\u002F@microsoft\u002Frayfin-data\u002Fassets\u002Fdocs\u002Findex.md`. The render-time\nhazard below is React's, and is this skill's to state.)\n\n```tsx\n\u002F\u002F Both can throw or render invalid React children.\nif (task.description.startsWith('Starter task')) {\n  \u002F\u002F ...\n}\nreturn \u003Cspan>{task.metadata}\u003C\u002Fspan>;\n```\n\nGuard optional reads and convert known values to strings.\nDate-only values need an explicit UTC formatter so a value such as `2026-10-15` cannot display as the previous day west of UTC:\n\n```tsx\nconst dateOnlyFormatter = new Intl.DateTimeFormat(undefined, {\n  dateStyle: 'medium',\n  timeZone: 'UTC',\n});\n\nfunction formatDateOnly(value: unknown): string {\n  if (typeof value !== 'string' || !\u002F^\\d{4}-\\d{2}-\\d{2}$\u002Fu.test(value)) {\n    return 'Not set';\n  }\n\n  const parsed = new Date(`${value}T00:00:00Z`);\n  return Number.isNaN(parsed.getTime())\n    ? 'Not set'\n    : dateOnlyFormatter.format(parsed);\n}\n```\n\nFor arrays, map the intended fields to strings.\nFor objects, render selected properties or an app-specific summary instead of relying on implicit conversion or dumping raw JSON.\nFormat numbers explicitly for their units and provide a fallback for missing or invalid values.\n\n## Keep saved rows visible\n\nA created or updated row should render from its own returned fields.\nDo not gate its title, date, status, or other saved values on unrelated live-source readiness.\n\nChoose a default filter that includes a just-created row, or explain clearly why the active filter excludes it.\nFor date-only filters, compare normalized date strings or use UTC fields.\nDo not parse a date-only value into local time before deciding whether it belongs in a month or day.\n\nThe optimistic row must be replaced by the returned row, which the kit already does.\nDo not add a field-by-field readback assertion that can reinterpret rounding, normalized dates, or service defaults as a failed save.\n\n## Checklist\n\n- [ ] Loading, empty, and error states all render — not just the happy path\n- [ ] Mutations are optimistic and roll back visibly on failure\n- [ ] Submit disabled while pending; success UI follows the actual operation or hook state contract\n- [ ] Input limits match the entity's `@text({ max })`\n- [ ] Every input has a label\n- [ ] Destructive actions confirm, and name the thing\n- [ ] Per-user data enforced by a `@role` policy, not a client filter\n- [ ] Seeding is idempotent and user-initiated\n- [ ] Optional fields and non-primitive JSX values are normalized before rendering\n- [ ] Date-only display and filtering cannot shift with the browser timezone\n- [ ] Default filters include a newly created row\n- [ ] Saved fields do not depend on unrelated live-source state\n","# CRUD UI — a list people can actually use\n\n`data-modeling` gives you entities and a typed client. This gives you the interface\nover them, and the states that separate a demo from something usable.\n\n**Prerequisites:** entities defined per `data-modeling`.\nKeep the template's dynamic sign-in flow and app-wide auth gate intact, including outside the portal.\n\n> **The client API is the package's, not this skill's.** For the query chain,\n> mutations, filtering, `findById`, and relationship writes, read\n> `node_modules\u002F@microsoft\u002Frayfin-guide\u002Fassets\u002Fdocs\u002Fdata\u002Fgraphql.md`. For paging,\n> cursors and text-field nullability, read\n> `node_modules\u002F@microsoft\u002Frayfin-data\u002Fassets\u002Fdocs\u002Findex.md`. Those move between\n> releases; the React patterns below do not.\n\nCopy the two files this skill ships into the app:\n\n```text\n.agents\u002Fskills\u002Fcrud-ui\u002Fkit\u002Fcomponents\u002FuseCrud.ts   ->  packages\u002Ffrontend\u002Fsrc\u002Fcomponents\u002FuseCrud.ts\n.agents\u002Fskills\u002Fcrud-ui\u002Fkit\u002Fcomponents\u002Fstates.tsx   ->  packages\u002Ffrontend\u002Fsrc\u002Fcomponents\u002Fstates.tsx\n```\n\n## The shape\n\n`useCrud` owns state and mutations; you own rendering. Give it four functions\nbound to your entity and it returns items plus optimistic `create` \u002F `update` \u002F\n`remove`. The four functions are ordinary client calls — see `data\u002Fgraphql.md`\nfor their exact signatures.\n\n```tsx\nimport { useCrud } from '@\u002Fcomponents\u002FuseCrud';\nimport { LoadingState, EmptyState, ErrorState } from '@\u002Fcomponents\u002Fstates';\nimport { getRayfinClient } from '@\u002Flib\u002Frayfin-client';\n\ninterface Task { id: string; title: string; done: boolean }\n\nexport function TaskList() {\n  const crud = useCrud\u003CTask>({\n    list: async () => (await getRayfinClient()).data.Task.select(['id', 'title', 'done']).execute(),\n    create: async (input) => (await getRayfinClient()).data.Task.create(input),\n    update: async (id, patch) => (await getRayfinClient()).data.Task.update({ id }, patch),\n    remove: async (id) => { await (await getRayfinClient()).data.Task.delete({ id }); },\n  });\n\n  if (crud.status === 'loading') return \u003CLoadingState \u002F>;\n  if (crud.status === 'error')   return \u003CErrorState message={crud.error!} onRetry={crud.refresh} \u002F>;\n\n  return (\n    \u003Cdiv className=\"space-y-3\">\n      {crud.error ? \u003CErrorState message={crud.error} onRetry={crud.clearError} \u002F> : null}\n\n      \u003CAddTaskForm\n        onAdd={(title) => { void crud.create({ title, done: false }); }}\n        disabled={crud.pending}\n      \u002F>\n\n      {crud.items.length === 0 ? (\n        \u003CEmptyState title=\"No tasks yet\" hint=\"Add your first one above.\" \u002F>\n      ) : (\n        \u003Cul className=\"space-y-2\">\n          {crud.items.map((task) => (\n            \u003Cli key={task.id} className=\"flex items-center gap-3 rounded-lg border border-black\u002F10 px-4 py-3 dark:border-white\u002F15\">\n              \u003Cinput\n                type=\"checkbox\"\n                checked={task.done}\n                onChange={(e) => crud.update(task.id, { done: e.target.checked })}\n              \u002F>\n              \u003Cspan className={task.done ? 'flex-1 line-through opacity-60' : 'flex-1'}>{task.title}\u003C\u002Fspan>\n              \u003Cbutton type=\"button\" onClick={() => crud.remove(task.id)}>Remove\u003C\u002Fbutton>\n            \u003C\u002Fli>\n          ))}\n        \u003C\u002Ful>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Why optimistic\n\nA Fabric round-trip is slow enough that the naive version feels broken — you click\nAdd, nothing happens, then a row appears. `useCrud` applies the change locally first\nand **rolls it back if the server rejects it**, surfacing the error. The UI tracks\nintent without lying about the outcome.\n\nTwo details worth knowing: a created row gets a temporary `pending-…` id so React\nkeys stay stable until the real row arrives, and a failed delete restores the row **in\nits original position**, not appended at the end.\n\n### Mutation result contract\n\nThe kit merges the row returned by create or update into local state.\nTreat that returned row as the service's authoritative representation, including normalized dates, rounded values, generated fields, and defaults.\nDo not refetch only to compare every field with the submitted input, and do not turn a successful mutation into a failure because the readback differs.\n\n`useCrud.create`, `useCrud.update`, and `useCrud.remove` catch operation errors, store the message in `crud.error`, roll back optimistic state where needed, and return `Promise\u003Cvoid>`.\nFulfillment of that wrapper promise is not proof that the save succeeded.\nUse `crud.pending`, `crud.error`, and the rendered item state for kit-driven UI feedback.\nWhen code calls a lower-level mutation directly, follow that operation's documented success and error contract instead of assuming that resolution or rejection means the same thing across APIs.\n\n## Forms\n\nControlled inputs, validation before submit, and a disabled button while pending:\n\n```tsx\nfunction AddTaskForm({ onAdd, disabled }: { onAdd(title: string): void; disabled?: boolean }) {\n  const [title, setTitle] = useState('');\n  const trimmed = title.trim();\n\n  return (\n    \u003Cform\n      onSubmit={(e) => { e.preventDefault(); if (!trimmed) return; onAdd(trimmed); }}\n      className=\"flex gap-2\"\n    >\n      \u003Clabel htmlFor=\"new-task\" className=\"sr-only\">Task\u003C\u002Flabel>\n      \u003Cinput\n        id=\"new-task\"\n        value={title}\n        onChange={(e) => setTitle(e.target.value)}\n        placeholder=\"Add a task\"\n        maxLength={200}\n        className=\"flex-1 rounded-lg border border-black\u002F15 px-3 py-2 dark:border-white\u002F20\"\n      \u002F>\n      \u003Cbutton type=\"submit\" disabled={disabled || !trimmed} className=\"rounded-lg px-4 py-2 font-medium disabled:opacity-50\">\n        Add\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}\n```\n\n- **Match the entity's constraints.** `@text({ max: 200 })` in the schema means\n  `maxLength={200}` here. Client validation that disagrees with the server produces\n  errors the user can't act on.\n- **Do not clear the field from an awaited `useCrud` wrapper alone.**\n  Its `Promise\u003Cvoid>` fulfills after either success or internally handled failure.\n  Keep the input on error, and clear it only from explicit success state or an operation contract that reports success directly.\n- **Every input needs a label** — `sr-only` when the design has no room for one.\n\n## Deleting\n\nUse `ConfirmDelete` for anything irreversible. It requires you to name the item,\nbecause \"Delete this item?\" is a prompt people click through without reading.\n\n```tsx\n{confirming === task.id ? (\n  \u003CConfirmDelete\n    label={task.title}\n    pending={crud.pending}\n    onCancel={() => setConfirming(null)}\n    onConfirm={() => { crud.remove(task.id); setConfirming(null); }}\n  \u002F>\n) : (\n  \u003Cbutton type=\"button\" onClick={() => setConfirming(task.id)}>Delete\u003C\u002Fbutton>\n)}\n```\n\nSkip confirmation only for genuinely cheap, reversible toggles.\n\n## Per-user data\n\nIf each person should see only their own rows, this is **server-side** work, not a\nfilter in the UI. Add the `@role` policy per `data-modeling` § per-user data — the\ndecorator and policy DSL are documented in\n`node_modules\u002F@microsoft\u002Frayfin-core\u002Fassets\u002Fdocs\u002Fpermissions.md` — and stamp the\nowner column from the session on create:\n\n```ts\nconst session = client.auth.getSession();\nif (!session.isAuthenticated || !session.user) throw new Error('Not signed in.');\ncreate: (input) => client.data.Task.create({ ...input, owner_id: session.user.id }),\n```\n\nThe column name has to match the one the policy compares against — the\n`data-modeling` pack seeds `owner_id`. Stamping a differently-named field leaves\nthe real one unset, and the policy then refuses every row.\n\n**Never** filter by owner only in the client — the rows would still be fetchable.\n\n## Seed data\n\nReal-feeling content on first run makes an empty app legible. Seed **once, on\ndemand** — never on every mount, which duplicates rows on refresh:\n\n```tsx\nasync function seedIfEmpty() {\n  const existing = await client.data.Task.select(['id']).execute();\n  if (existing.length > 0) return;\n  for (const title of ['Read the brief', 'Draft the plan']) {\n    await client.data.Task.create({\n      title,\n      done: false,\n      owner_id: session.user.id,\n    });\n  }\n}\n```\n\nPrefer wiring this to an explicit \"Add sample tasks\" button in the empty state, so it\nis the user's choice and obviously distinct from real content. Seeded rows are real\nrows in their database — if they are invented *figures* rather than starter content,\nlabel them visibly in the UI as sample data, per the Critical Rules in `AGENTS.md`.\n\n## Rendering a row safely\n\nTreat every value crossing into JSX as untrusted display input.\nA row written before a field existed may return `null` or `undefined`.\nA date can arrive as a `Date`, and objects or arrays are never valid React text without an app-specific conversion.\nDo not interpolate a raw `Date`, object, or array.\n\n(Optional vs. required text nullability is a package behaviour — see\n`node_modules\u002F@microsoft\u002Frayfin-data\u002Fassets\u002Fdocs\u002Findex.md`. The render-time\nhazard below is React's, and is this skill's to state.)\n\n```tsx\n\u002F\u002F Both can throw or render invalid React children.\nif (task.description.startsWith('Starter task')) {\n  \u002F\u002F ...\n}\nreturn \u003Cspan>{task.metadata}\u003C\u002Fspan>;\n```\n\nGuard optional reads and convert known values to strings.\nDate-only values need an explicit UTC formatter so a value such as `2026-10-15` cannot display as the previous day west of UTC:\n\n```tsx\nconst dateOnlyFormatter = new Intl.DateTimeFormat(undefined, {\n  dateStyle: 'medium',\n  timeZone: 'UTC',\n});\n\nfunction formatDateOnly(value: unknown): string {\n  if (typeof value !== 'string' || !\u002F^\\d{4}-\\d{2}-\\d{2}$\u002Fu.test(value)) {\n    return 'Not set';\n  }\n\n  const parsed = new Date(`${value}T00:00:00Z`);\n  return Number.isNaN(parsed.getTime())\n    ? 'Not set'\n    : dateOnlyFormatter.format(parsed);\n}\n```\n\nFor arrays, map the intended fields to strings.\nFor objects, render selected properties or an app-specific summary instead of relying on implicit conversion or dumping raw JSON.\nFormat numbers explicitly for their units and provide a fallback for missing or invalid values.\n\n## Keep saved rows visible\n\nA created or updated row should render from its own returned fields.\nDo not gate its title, date, status, or other saved values on unrelated live-source readiness.\n\nChoose a default filter that includes a just-created row, or explain clearly why the active filter excludes it.\nFor date-only filters, compare normalized date strings or use UTC fields.\nDo not parse a date-only value into local time before deciding whether it belongs in a month or day.\n\nThe optimistic row must be replaced by the returned row, which the kit already does.\nDo not add a field-by-field readback assertion that can reinterpret rounding, normalized dates, or service defaults as a failed save.\n\n## Checklist\n\n- [ ] Loading, empty, and error states all render — not just the happy path\n- [ ] Mutations are optimistic and roll back visibly on failure\n- [ ] Submit disabled while pending; success UI follows the actual operation or hook state contract\n- [ ] Input limits match the entity's `@text({ max })`\n- [ ] Every input has a label\n- [ ] Destructive actions confirm, and name the thing\n- [ ] Per-user data enforced by a `@role` policy, not a client filter\n- [ ] Seeding is idempotent and user-initiated\n- [ ] Optional fields and non-primitive JSX values are normalized before rendering\n- [ ] Date-only display and filtering cannot shift with the browser timezone\n- [ ] Default filters include a newly created row\n- [ ] Saved fields do not depend on unrelated live-source state\n",{"name":5,"description":7},"main","0979439a7a0806abd0ee48c2619b37f297ae1324","samples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Fcrud-ui",77,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-12 01:21:10",{"source":8,"skillName":5,"commands":41,"prompt":51,"zipAvailable":52},[42,45,48],{"tool":43,"command":44},"npx","npx skills add microsoft\u002Frayfin --skill crud-ui",{"tool":46,"command":47},"bunx","bunx skills add microsoft\u002Frayfin --skill crud-ui",{"tool":49,"command":50},"pnpm","pnpm dlx skills add microsoft\u002Frayfin --skill crud-ui","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Fcrud-ui\u002FSKILL.md",true,[54,60,66,72],{"path":55,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":58,"sourceHtmlUrl":59},"kit\u002Fcomponents\u002Fstates.tsx","OTHER","其他",3667,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Fcrud-ui\u002Fkit\u002Fcomponents\u002Fstates.tsx",{"path":61,"resourceType":62,"resourceTypeDesc":63,"sizeBytes":64,"sourceHtmlUrl":65},"kit\u002Fcomponents\u002FuseCrud.ts","SCRIPT","脚本",5449,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Fcrud-ui\u002Fkit\u002Fcomponents\u002FuseCrud.ts",{"path":67,"resourceType":68,"resourceTypeDesc":69,"sizeBytes":70,"sourceHtmlUrl":71},"references\u002Fevidence-and-tools.md","REFERENCE","参考文档",4315,"https:\u002F\u002Fgithub.com\u002Fopenclaw\u002Fclawhub\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fclawhub-clawscan\u002Freferences\u002Fevidence-and-tools.md",{"path":73,"resourceType":68,"resourceTypeDesc":69,"sizeBytes":74,"sourceHtmlUrl":75},"references\u002Fupstream-reporting.md",4121,"https:\u002F\u002Fgithub.com\u002Fopenclaw\u002Fclawhub\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fclawhub-clawscan\u002Freferences\u002Fupstream-reporting.md",[77,90,108,117,134,142],{"skillNo":78,"name":79,"nameZh":6,"description":80,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":81,"categories":82,"keywords":83,"publishedAt":26},"01M699YEY2QDVPKTZS3HR9AX84","rayfin-getting-started","Use when starting or creating a NEW Rayfin app, or when a Rayfin task comes up and you are not yet inside a Rayfin project. Gets you into a project with the Rayfin CLI, then hands off to the authoritative, version-locked in-project rayfin skill\u002Fdocs that own all in-project work. Triggers: build a Rayfin app, start a Rayfin project, create a new Rayfin app, create-rayfin, npm create @microsoft\u002Frayfin, rayfin init, scaffold rayfin, rayfin CLI, rayfin template, universal app, awesome-rayfin gallery, get started with Rayfin","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fskills\u002Frayfin-getting-started\u002FSKILL.md",[],[84,85,86,87,9,88,89],"rayfin getting started","rayfin","scaffold","init","docs","cli",{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":104,"publishedAt":26},"01M699YCN4ZDW2AMDP4NVJ2QGC","visuals","Use when user wants to incorporate charts, graphs, data grid,  or other visual representations of data into their project. Use VegaVisual and DataGrid components to create these visuals,  utilizing the shared DataTable input, formatting, theme, and interactivity. Covers the onInteraction selection\u002Fclick events host apps consume, named multi-table data input for layered overlays and reference lines, and Vega-Lite native selections. Triggers: chart, graph, plot, dashboard, KPI, metric, trend, scorecard, data grid, visualize.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Fvisuals\u002FSKILL.md",[96,101],{"categoryNo":97,"categoryName":98,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000013","分析与可视化","SCENARIO","使用场景",{"categoryNo":102,"categoryName":103,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000033","数据分析师",[92,105,106,107],"components","dashboard","visualize",{"skillNo":109,"name":110,"nameZh":6,"description":111,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":112,"categories":113,"keywords":114,"publishedAt":26},"01M699YBEP146MJZJNN8YEBZ2G","schema-discovery","Explore Power BI semantic model schemas using DAX INFO functions. Progressive metadata discovery strategy, scope estimation, and narrowing techniques for tables, columns, measures, and relationships.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Fschema-discovery\u002FSKILL.md",[],[115,116],"schema discovery","dax",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":130,"publishedAt":26},"01M699Y9E80WYFKD1S4MT4B1CS","query-design","Separates DAX data-fetching from TypeScript presentation. Guides when to use  DAX vs. TypeScript vs. Vega-Lite for aggregation, total rows, FORMAT(), SELECTCOLUMNS,  BLANK handling, filtering, multi-grain queries, cross-filtering and cross-highlight subset\u002Foverlay queries, and format strings.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Fquery-design\u002FSKILL.md",[123,126,129],{"categoryNo":124,"categoryName":125,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000003","演示文稿",{"categoryNo":127,"categoryName":128,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[131,132,116,133],"query design","design","typescript",{"skillNo":135,"name":136,"nameZh":6,"description":137,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":140,"publishedAt":26},"01M699Y7CKZ55QSYFE827RRVJE","functions-capability","Apply and build the Universal App functions capability. Use for trusted server-side logic, secrets, external APIs, custom model endpoints, published Fabric data agents, webhooks, or privileged operations.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Ffunctions-capability\u002FSKILL.md",[],[141],"functions capability",{"skillNo":143,"name":144,"nameZh":6,"description":145,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":146,"categories":147,"keywords":148,"publishedAt":26},"01M699Y5R87YE620T78Z30KQSA","fabric-sdk","How to query Fabric data from a running app through the Rayfin connectors client. Use getRayfinClient().connectors.\u003Cname>.executeQuery to run DAX against a semantic model, and toQueryResult to normalize the response.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Frayfin\u002Fblob\u002Fmain\u002Fsamples\u002Funiversal-app\u002Ftemplate\u002F.agents\u002Fskills\u002Ffabric-sdk\u002FSKILL.md",[],[149,150,116,151,85,152],"sdk","fabric sdk","rayfin connectors","connectors"]