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-group

v0.1.2

Published

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

Downloads

406

Readme

@qfei-design/make-app-group

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

设计边界

包负责:

  • 从运行时字段中只选择 capabilities.groupable === true 的字段。
  • 使用有序 { fieldKey, order }[] 表示分组层级,第一项是一级分组。
  • 校验字段唯一、方向只能为 asc | desc,最多 3 个分组条件。
  • capabilities.sortable === true 的字段可切换分组方向,其余字段固定升序并禁用方向按钮。
  • 管理已应用值之外的面板草稿、显式添加的空草稿行、删除、清空、方向切换和拖拽重排。
  • onConfirm 异步持久化期间锁定面板;失败时保留草稿和错误。
  • 持久化成功且 resetKey 未变化时才同步调用 onApplied
  • 通过 openWithField(fieldKey, order?) 连接宿主表头入口和同一份面板草稿。
  • 提供可注入组件合同,以及 createAntdRecordGroupComponents 官方适配器。

宿主负责:

  • 加载并传入标准化运行时字段;包不自动探测字段和 UI 框架。
  • 持有已应用分组值和当前对象的 resetKey
  • 持有工具栏触发器以及 Popover、Modal 或 Drawer 等外层容器。
  • onConfirm 中只保存 Entity Preset 的 group
  • 在同步 onApplied 中只更新已应用分组值,不能返回 Promise。
  • entityKey + appliedGroup 为请求键加载分组数据和叶子明细,并处理取消、乱序和失败。
  • 负责 Service 调用、Canvas 表格刷新、边界日志、错误脱敏和用户错误文案转换。

包不生成 CEL,不请求 Data API,不保存 Preset,不渲染外层 Popover,也不做 Canvas 表格 渲染。外层容器的挂载点、关闭规则和工具栏触发器都属于宿主页面。

安装

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

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

Ant Design 宿主示例

import { GroupOutlined } from "@ant-design/icons";
import { Button, Popover } from "antd";
import { useEffect, useMemo, useState } from "react";
import {
  getGroupableRecordFields,
  RecordGroupPanel,
  useRecordGroupController,
  type RecordGroupApplyErrorHandler,
  type RecordGroupField,
  type RecordGroupValue,
} from "@qfei-design/make-app-group/react";
import {
  createAntdRecordGroupComponents,
} from "@qfei-design/make-app-group/adapters/antd";
import "@qfei-design/make-app-group/styles.css";

type GroupResult = Awaited<ReturnType<typeof requestGroupData>>;

type Props = {
  entityKey: string;
  fields: RecordGroupField[];
  appliedGroup: RecordGroupValue;
  onAppliedGroupChange: (value: RecordGroupValue) => void;
  onGroupApplyError: RecordGroupApplyErrorHandler;
  onGroupDataError: (error: unknown) => void;
  onGroupDataLoaded: (result: GroupResult) => void;
};

export function GroupEntry({
  entityKey,
  fields,
  appliedGroup,
  onAppliedGroupChange,
  onGroupApplyError,
  onGroupDataError,
  onGroupDataLoaded,
}: Props) {
  const [open, setOpen] = useState(false);
  const components = useMemo(
    () => createAntdRecordGroupComponents(),
    [],
  );
  const groupableFields = useMemo(
    () => getGroupableRecordFields(fields),
    [fields],
  );
  const controller = useRecordGroupController({
    fields,
    value: appliedGroup,
    resetKey: entityKey,
    onOpenChange: setOpen,
    getErrorMessage: () => "分组保存失败,请重试",
    onApplyError: onGroupApplyError,
    onConfirm: async (nextGroup) => {
      await saveEntityPreset(entityKey, { group: nextGroup });
    },
    onApplied: (nextGroup) => {
      onAppliedGroupChange(nextGroup);
    },
  });

  useEffect(() => {
    const requestController = new AbortController();
    void requestGroupData({
      entityKey,
      group: appliedGroup,
      signal: requestController.signal,
    }).then(
      (result) => {
        if (!requestController.signal.aborted) {
          onGroupDataLoaded(result);
        }
      },
      (error: unknown) => {
        if (!requestController.signal.aborted) {
          onGroupDataError(error);
        }
      },
    );

    return () => requestController.abort();
  }, [
    appliedGroup,
    entityKey,
    onGroupDataError,
    onGroupDataLoaded,
  ]);

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

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

saveEntityPresetrequestGroupData 是宿主接口示意,不属于本包。宿主必须让 onConfirm 只负责持久化,并在失败时 reject;控制器才会保留面板和草稿。 onApplied 只会在保存成功且 resetKey 未变化时同步调用,必须只更新受控的 appliedGroup,不能返回 Promise。分组数据和明细请求应由以 entityKey + appliedGroup 为键的 effect 或请求库接管,并具备取消、旧请求丢弃和错误处理。

onApplyError 是必填的宿主错误边界,用于记录已持久化但应用状态或关闭动作失败的 异常,可以同步返回或返回 Promise;日志不得包含记录数据、Cookie、Authorization 或 token。

表头联动

表头分组入口不能直接请求分组数据。它只更新并打开同一个草稿:

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

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

自定义组件

非 Ant Design 项目向 RecordGroupPanel 传入 RecordGroupComponents

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

组件合同只描述交互语义,不要求宿主暴露具体 UI 库实例。拖拽手柄和 dnd-kit 上下文由包维护, 宿主不需要安装或编排拖拽插件。直接使用 RecordGroupPanel 时必须提供可同步或异步的 onConfirmError;使用控制器返回的 panelProps 时,该错误边界已由控制器注入。 若直接面板的错误边界自身抛错或 reject,面板会显示“分组操作失败,请重试”,不会产生未处理拒绝。 标准面板会在底部展示“添加条件组”,空分组默认展示一行草稿;达到三级上限时不再展示该按钮, 没有可选字段、已有空草稿行或保存中时,该按钮禁用。 如果通过 labels.help 传入自定义 ReactNode 帮助内容,建议同时传入 labels.helpAriaLabel; 未传入时,帮助图标使用“分组条件说明”作为中性可访问名称,避免读出与视觉提示不一致的默认规则。

样式

在宿主入口导入一次:

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

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

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

稳定入口

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

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