npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@qfei-design/make-app-sort

v0.1.0

Published

Headless Make record-sort model, React drag-sort panel, and optional host component adapters.

Readme

@qfei-design/make-app-sort

Make App 列表排序的可复用 npm 包。它提供无框架排序模型、React 草稿控制器、 基于 dnd-kit 的多字段拖拽面板、Ant Design 官方适配器和内部样式。

设计结论

包负责什么

  • 从运行时字段中只选择 capabilities.sortable === true 的字段。
  • 使用有序的 { fieldKey, order }[] 表示优先级,第一项优先级最高。
  • 校验字段必选、字段唯一、方向只能为 asc | desc,并且最多 5 级排序。
  • 管理“已应用值之外”的面板草稿、添加、删除、清空、排序方向和拖拽重排。
  • onConfirm 异步持久化期间锁定面板;失败时保留草稿和错误。
  • 持久化成功且对象未切换时才同步调用 onApplied,避免旧对象结果污染新对象。
  • 通过 openWithField(fieldKey, order?) 连接表头排序入口和同一份面板草稿。
  • 提供可注入组件合同,以及 createAntdRecordSortComponents 官方适配器。

宿主负责什么

  • 加载并传入标准化运行时字段,不使用字段类型白名单代替能力判断。
  • 持有已经应用的排序值和当前对象的 resetKey
  • 持有工具栏触发器以及 Popover、Modal 或 Drawer 等外层容器。
  • onConfirm 中只保存 Entity Preset 的 sort
  • 在同步 onApplied 中只更新已应用值。
  • entityKey + appliedSort 为请求键加载 records,并处理取消、乱序和失败。
  • 把同一份已应用排序传给 records 接口,不在前端对当前页数据做本地排序。
  • 持有 CanvasTable 表头菜单,并调用控制器的 openWithField
  • 负责 Service 调用、边界日志、错误脱敏和用户错误文案转换。

包不自动探测宿主使用哪套 UI 框架。自动探测会让依赖、样式和 portal 行为不可控。 Ant Design 项目使用官方适配器作为默认方案;其他设计系统实现 RecordSortComponents。包也不直接渲染外层 Popover,因为挂载位置、层级、关闭规则 和工具栏触发器都属于宿主页面。

安装

pnpm add @qfei-design/make-app-sort

React 面板使用 dnd-kit,依赖由本包声明。React 是 peer dependency。使用 Ant Design 适配器时,宿主还需要安装 antd@ant-design/icons

Ant Design 宿主示例

import { SortAscendingOutlined } from "@ant-design/icons";
import { Button, Popover } from "antd";
import { useEffect, useMemo, useState } from "react";
import {
  getSortableRecordFields,
  RecordSortPanel,
  useRecordSortController,
  type RecordSortApplyErrorHandler,
  type RecordSortField,
  type RecordSortValue,
} from "@qfei-design/make-app-sort/react";
import {
  createAntdRecordSortComponents,
} from "@qfei-design/make-app-sort/adapters/antd";
import "@qfei-design/make-app-sort/styles.css";

type RecordsResult = Awaited<ReturnType<typeof requestRecords>>;

type Props = {
  entityKey: string;
  fields: RecordSortField[];
  appliedSort: RecordSortValue;
  onAppliedSortChange: (value: RecordSortValue) => void;
  onRecordsError: (error: unknown) => void;
  onRecordsLoaded: (result: RecordsResult) => void;
  onSortApplyError: RecordSortApplyErrorHandler;
};

export function SortEntry({
  entityKey,
  fields,
  appliedSort,
  onAppliedSortChange,
  onRecordsError,
  onRecordsLoaded,
  onSortApplyError,
}: Props) {
  const [open, setOpen] = useState(false);
  const components = useMemo(
    () => createAntdRecordSortComponents(),
    [],
  );
  const sortableFields = useMemo(
    () => getSortableRecordFields(fields),
    [fields],
  );
  const controller = useRecordSortController({
    fields,
    value: appliedSort,
    resetKey: entityKey,
    onOpenChange: setOpen,
    getErrorMessage: () => "排序保存失败,请重试",
    onApplyError: onSortApplyError,
    onConfirm: async (nextSort) => {
      await saveEntityPreset(entityKey, { sort: nextSort });
    },
    onApplied: (nextSort) => {
      onAppliedSortChange(nextSort);
    },
  });

  useEffect(() => {
    const requestController = new AbortController();
    void requestRecords({
      entityKey,
      signal: requestController.signal,
      sort: appliedSort,
    }).then(
      (result) => {
        if (!requestController.signal.aborted) {
          onRecordsLoaded(result);
        }
      },
      (error: unknown) => {
        if (!requestController.signal.aborted) {
          onRecordsError(error);
        }
      },
    );

    return () => requestController.abort();
  }, [
    appliedSort,
    entityKey,
    onRecordsError,
    onRecordsLoaded,
  ]);

  if (sortableFields.length === 0) return null;

  return (
    <Popover
      destroyOnHidden
      open={open}
      placement="bottom"
      styles={{ content: { padding: 0 } }}
      content={
        <RecordSortPanel
          components={components}
          {...controller.panelProps}
        />
      }
      onOpenChange={(nextOpen) => {
        if (nextOpen) controller.beginDraft();
        else controller.discardDraft();
      }}
    >
      <Button
        aria-expanded={open}
        aria-haspopup="dialog"
        icon={<SortAscendingOutlined />}
      >
        {appliedSort.length > 0
          ? `${appliedSort.length} 排序`
          : "排序"}
      </Button>
    </Popover>
  );
}

saveEntityPresetrequestRecords 是宿主接口示意,不属于本包。宿主必须让 onConfirm 只负责持久化,并在失败时 reject;控制器才会保留面板和草稿。 onApplied 只会在保存成功且 resetKey 未变化时同步调用,必须只更新受控的 appliedSort,不能返回 Promise。records 请求应由以 entityKey + appliedSort 为键的 effect 或请求库接管,并具备取消、旧请求丢弃和错误处理。 onApplyError 是必填的宿主错误边界,用于记录已持久化但应用状态或关闭动作失败的 异常,可以同步返回或返回 Promise;日志不得包含记录数据、Cookie、Authorization 或 token。

TypeScript 的 void handler 允许预先标注类型的异步函数通过赋值检查,因此包仍会在 运行时观察 onApplied 和关闭回调的真实返回值。意外返回的 Promise 会被等待,其 rejection 按 apply-failed 处理,不会形成未处理拒绝;这只是防御措施,不改变 onApplied 必须同步更新状态的宿主合同。若 onApplyError 自身失败,控制器会把原始 应用异常与上报异常合并为 AggregateError 返回,并显示安全兜底文案。应用和关闭 结果确定后,控制器会先释放 UI 提交锁,再等待异步 onApplyError;错误日志或监控 请求不会阻止用户重新打开排序草稿。

appliedSort 必须由对象 Preset 水合层按 entityKey 受控提供,不能只在组件首次 挂载时复制 initialSort。加载页面时,宿主应先完成运行时 schema 和 Preset 水合, 再挂载上述 records 请求链路。

表头联动

表头升序/降序不能直接请求 records。它只更新并打开同一个草稿:

function handleHeaderSort(fieldKey: string, order: "asc" | "desc") {
  const result = controller.openWithField(fieldKey, order);
  if (result.status === "field-unavailable") return;
  closeHeaderMenu();
}

openWithField 会更新已存在字段、填充空行或添加新行。达到五级上限时返回 limit-reached,面板仍会打开并显示本地错误;提交进行中返回 busy,不会改动 草稿。

自定义组件

非 Ant Design 项目向 RecordSortPanel 传入 RecordSortComponents

  • Button
  • IconButton
  • Select
  • Tooltip
  • icons:添加、升序、降序、删除、拖拽和帮助图标

组件合同只描述交互语义,不要求宿主暴露具体 UI 库实例。拖拽手柄和 dnd-kit 上下文由包维护,宿主不需要安装或编排拖拽插件。 直接使用 RecordSortPanel 时必须提供可同步或异步的 onConfirmError;使用控制器 返回的 panelProps 时,该错误边界已由控制器注入。若直接面板的错误边界自身抛错或 reject,面板会显示“排序操作失败,请重试”,不会产生未处理拒绝。

样式

在宿主入口导入一次:

import "@qfei-design/make-app-sort/styles.css";

样式只覆盖 .make-app-sort 内部结构。面板默认宽度为 432px,可通过 --make-app-sort-panel-width 覆盖。外层 Popover、Modal 或 Drawer 的阴影、箭头、 z-index 和 portal 挂载点由宿主负责。

内部拖拽浮层默认 portal 到 document.body,默认 z-index 为 1100。特殊宿主可通过 getDragOverlayContainer 指定挂载容器,通过 dragOverlayZIndex 调整层级。 如果主题变量只定义在宿主局部容器上,应通过 dragOverlayClassName 给 portal 浮层附加主题类,并在该类上声明变量;SSR 环境自动使用内联回退。

稳定入口

  • @qfei-design/make-app-sort
  • @qfei-design/make-app-sort/react
  • @qfei-design/make-app-sort/adapters/antd
  • @qfei-design/make-app-sort/styles.css

不要从 srcdist 或其他内部路径导入。完整合同见 PUBLIC_API.md